diff --git a/lib/equal/orm/Entity.class.php b/lib/equal/orm/Entity.class.php index 836c89d8..6b9b50df 100644 --- a/lib/equal/orm/Entity.class.php +++ b/lib/equal/orm/Entity.class.php @@ -6,7 +6,10 @@ Licensed under GNU LGPL 3 license */ namespace equal\orm; - +use Exception; +use ReflectionClass; +use ReflectionMethod; +use ReflectionException; class Entity { /** * @var string @@ -103,4 +106,135 @@ public function getFullFilePath(): string { } return $filepath; } + + + public function updateMethod(string $methodName, array $newData) { + try { + $class = new ReflectionClass($this->full_name); + + if (!$class->hasMethod($methodName)) { + throw new Exception("missing_method", QN_ERROR_UNKNOWN); + } + + $method_code = " public static function $methodName() {\n" . + " return " . (empty($newData) ? '[]' : $this->arrayExport($newData, 4, 2, true)) . ";\n" . + " }"; + + $this->updateMethodCode($class, $methodName, $method_code); + } catch (ReflectionException $e) { + throw new Exception("reflection_error: " . $e->getMessage(), QN_ERROR_UNKNOWN); + } + } + + public function updateMethodLine(string $methodName, string $newKey, $defaultValue = []) { + try { + $class = new ReflectionClass($this->full_name); + + if (!$class->hasMethod($methodName)) { + throw new Exception("missing_method", QN_ERROR_UNKNOWN); + } + + $oldData = call_user_func([$this->full_name, $methodName]); + if (!is_array($oldData)) { + $oldData = []; + } + + if (!array_key_exists($newKey, $oldData)) { + $oldData[$newKey] = $defaultValue; + } + + $method_code = " public static function $methodName() {\n" . + " return " . $this->arrayExport($oldData, 4, 2, true) . ";\n" . + " }"; + + $this->updateMethodCode($class, $methodName, $method_code); + } catch (ReflectionException $e) { + throw new Exception("reflection_error: " . $e->getMessage(), QN_ERROR_UNKNOWN); + } + } + + + private function updateMethodCode(ReflectionClass $class, string $methodName, string $newCode) { + try { + $file = $class->getFileName(); + if (!$file || !file_exists($file)) { + throw new Exception("File not found: $file", QN_ERROR_UNKNOWN); + } + + $code = file_get_contents($file); + if ($code === false) { + throw new Exception("Failed to read file: $file", QN_ERROR_UNKNOWN); + } + + $lines = explode("\n", $code); + + try { + $method = new ReflectionMethod($class->getName(), $methodName); + } catch (ReflectionException $e) { + throw new Exception("Method $methodName not found in class " . $class->getName(), QN_ERROR_UNKNOWN); + } + + $start_index = $method->getStartLine() - 1; + $end_index = $method->getEndLine() - 1; + + if ($start_index < 0 || $end_index < 0 || $end_index < $start_index) { + throw new Exception("Invalid method boundaries for $methodName", QN_ERROR_UNKNOWN); + } + + $result = ''; + foreach ($lines as $index => $line) { + if ($index < $start_index) { + $result .= $line . "\n"; + } elseif ($index == $start_index) { + $result .= $newCode . "\n"; + } elseif ($index > $end_index) { + $result .= $line . "\n"; + } + } + + if (file_put_contents($file, rtrim($result) . "\n") === false) { + throw new Exception("Failed to write to file: $file", QN_ERROR_UNKNOWN); + } + } catch (Exception $e) { + error_log("Error in updateMethodCode: " . $e->getMessage()); + throw $e; + } + } + + + + + private function arrayExport($array, $indent_spaces = 4, $pad_indents = 0, $ignore_first_indent = false) { + if (!is_array($array)) { + return ''; + } + + $export = var_export($array, true); + + $patterns = [ + "/array \(/" => '[', + "/^([ ]*)\)(,?)$/m" => '$1]$2', + "/=>[ ]?\n[ ]+\[/" => '=> [', + "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3', + "/[0-9]+ => /" => '' + ]; + + $result = preg_replace(array_keys($patterns), array_values($patterns), $export); + if (empty($result)) { + return ''; + } + + $lines = explode("\n", $result); + foreach ($lines as $index => $line) { + if (!$ignore_first_indent || $index > 0) { + $code = ltrim($line); + $indents = (strlen($line) - strlen($code)) / 2; + $lines[$index] = str_pad('', $pad_indents * $indent_spaces, ' ') . + str_pad('', $indents * $indent_spaces, ' ') . + $code; + } + } + + return implode("\n", $lines); + } } \ No newline at end of file diff --git a/packages/core/actions/config/create-actions.php b/packages/core/actions/config/create-actions.php new file mode 100644 index 00000000..ac78c398 --- /dev/null +++ b/packages/core/actions/config/create-actions.php @@ -0,0 +1,73 @@ + + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Cédric FRANCOYS + Licensed under GNU LGPL 3 license +*/ + +list($params, $providers) = eQual::announce([ + 'description' => "Add an empty actions to the given class by creating a `getActions()` method (if not defined yet).", + 'params' => [ + 'entity' => [ + 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').', + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text', + 'charset' => 'utf-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected', + 'groups' => ['admins'] + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list($context, $orm) = [ $providers['context'], $providers['orm'] ]; + +// force class autoload +$entity = $orm->getModel($params['entity']); +if(!$entity) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$class = new ReflectionClass($entity::getType()); +if($class->getMethod('getActions')->class == $entity::getType()) { + throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM); +} + +$file = $class->getFileName(); +$code = file_get_contents($file); + +// generate replacement code for the getActions method +$actions_code = ''. + " public static function getActions() {\n". + " return [];\n". + " }\n". + "\n"; + +// find the closing curly-bracket of the class +$pos = strrpos($code, '}'); + +if($pos === false) { + throw new Exception('malformed_file', QN_ERROR_UNKNOWN); +} + +$result = substr_replace($code, $actions_code, $pos, 0); + +// write back the code to the source file +if(file_put_contents($file, rtrim($result)."\n") === false) { + throw new Exception('io_error', QN_ERROR_UNKNOWN); +} + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/config/create-policies.php b/packages/core/actions/config/create-policies.php new file mode 100644 index 00000000..b441b54a --- /dev/null +++ b/packages/core/actions/config/create-policies.php @@ -0,0 +1,73 @@ + + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Cédric FRANCOYS + Licensed under GNU LGPL 3 license +*/ + +list($params, $providers) = eQual::announce([ + 'description' => "Add an empty policies to the given class by creating a `getPolicies()` method (if not defined yet).", + 'params' => [ + 'entity' => [ + 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').', + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text', + 'charset' => 'utf-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected', + 'groups' => ['admins'] + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list($context, $orm) = [ $providers['context'], $providers['orm'] ]; + +// force class autoload +$entity = $orm->getModel($params['entity']); +if(!$entity) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$class = new ReflectionClass($entity::getType()); +if($class->getMethod('getPolicies')->class == $entity::getType()) { + throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM); +} + +$file = $class->getFileName(); +$code = file_get_contents($file); + +// generate replacement code for the getPolicies method +$policies_code = ''. + " public static function getPolicies() {\n". + " return [];\n". + " }\n". + "\n"; + +// find the closing curly-bracket of the class +$pos = strrpos($code, '}'); + +if($pos === false) { + throw new Exception('malformed_file', QN_ERROR_UNKNOWN); +} + +$result = substr_replace($code, $policies_code, $pos, 0); + +// write back the code to the source file +if(file_put_contents($file, rtrim($result)."\n") === false) { + throw new Exception('io_error', QN_ERROR_UNKNOWN); +} + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/config/create-policy.php b/packages/core/actions/config/create-policy.php new file mode 100644 index 00000000..ef75bdd9 --- /dev/null +++ b/packages/core/actions/config/create-policy.php @@ -0,0 +1,58 @@ + "create a new policy", + 'help' => "This controller relies on the PHP binary. Ensure the PHP binary is present in the PATH.", + 'params' => [ + 'entity' => [ + 'description' => 'Name of the entity (class).', + 'type' => 'string', + 'required' => true + ], + 'name' => [ + 'description' => 'Name of the policy', + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text/plain', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ + +list($context, $orm) = [$providers['context'], $providers['orm']]; + + +$entity = new Entity($params['entity']); +$new_policy = $params['name']; +// force class autoload +$entityInstance = $orm->getModel($entity->getFullName()); +if(!$entityInstance) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$method_name = 'getPolicies' ; +$structure= [ +'description' =>'', +'function' => '' +]; +$class = new ReflectionClass($entityInstance::getType()); +if(!($class->getMethod($method_name)->class == $entityInstance::getType())) { + equal::run('do','core_config_create-policies',['entity' => $entity->getFullName()], true); +} + +$entity->updateMethodLine($method_name,$new_policy, $structure); + + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/config/create-roles.php b/packages/core/actions/config/create-roles.php new file mode 100644 index 00000000..d1a3c326 --- /dev/null +++ b/packages/core/actions/config/create-roles.php @@ -0,0 +1,73 @@ + + Some Rights Reserved, eQual framework, 2010-2024 + Original author(s): Cédric FRANCOYS + Licensed under GNU LGPL 3 license +*/ + +list($params, $providers) = eQual::announce([ + 'description' => "Add an empty roles to the given class by creating a `getRoles()` method (if not defined yet).", + 'params' => [ + 'entity' => [ + 'description' => 'Full name (including namespace) of the class to look into (e.g. \'core\\User\').', + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text', + 'charset' => 'utf-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected', + 'groups' => ['admins'] + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list($context, $orm) = [ $providers['context'], $providers['orm'] ]; + +// force class autoload +$entity = $orm->getModel($params['entity']); +if(!$entity) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$class = new ReflectionClass($entity::getType()); +if($class->getMethod('getRoles')->class == $entity::getType()) { + throw new Exception("duplicate_method", QN_ERROR_INVALID_PARAM); +} + +$file = $class->getFileName(); +$code = file_get_contents($file); + +// generate replacement code for the getRoles method +$actions_code = ''. + " public static function getRoles() {\n". + " return [];\n". + " }\n". + "\n"; + +// find the closing curly-bracket of the class +$pos = strrpos($code, '}'); + +if($pos === false) { + throw new Exception('malformed_file', QN_ERROR_UNKNOWN); +} + +$result = substr_replace($code, $actions_code, $pos, 0); + +// write back the code to the source file +if(file_put_contents($file, rtrim($result)."\n") === false) { + throw new Exception('io_error', QN_ERROR_UNKNOWN); +} + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/config/update-actions.php b/packages/core/actions/config/update-actions.php new file mode 100644 index 00000000..29e58cab --- /dev/null +++ b/packages/core/actions/config/update-actions.php @@ -0,0 +1,52 @@ + "Update the getActions() return function of an entity", + 'help' => "This controller relies on the PHP binary. Ensure the PHP binary is present in the PATH.", + 'params' => [ + 'entity' => [ + 'description' => 'Name of the entity (class).', + 'type' => 'string', + 'required' => true + ], + 'payload' => [ + 'description' => 'Actions definition.', + 'type' => 'array', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text/plain', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ + +list($context, $orm) = [$providers['context'], $providers['orm']]; + +$entity = new Entity($params['entity']); + +// force class autoload +$entityInstance = $orm->getModel($params['entity']); +if(!$entityInstance) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$method_name = 'getActions' ; +$class = new ReflectionClass($entityInstance::getType()); +if(!($class->getMethod($method_name)->class == $entityInstance::getType())) { + equal::run('do','core_config_create-roles',['entity' => $entity->getFullName()], true); +} + +$entity->updateMethod($method_name,$params['payload']); + +$context->httpResponse() + ->status(204) + ->send(); \ No newline at end of file diff --git a/packages/core/actions/config/update-policies.php b/packages/core/actions/config/update-policies.php new file mode 100644 index 00000000..76822c37 --- /dev/null +++ b/packages/core/actions/config/update-policies.php @@ -0,0 +1,54 @@ + "Update the getPolicies() return function of an entity", + 'help' => "This controller relies on the PHP binary. Ensure the PHP binary is present in the PATH.", + 'params' => [ + 'entity' => [ + 'description' => 'Name of the entity (class).', + 'type' => 'string', + 'required' => true + ], + 'payload' => [ + 'description' => 'Actions definition.', + 'type' => 'array', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'text/plain', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ + +list($context, $orm) = [$providers['context'], $providers['orm']]; + +$entity = new Entity($params['entity']); + +// force class autoload +$entityInstance = $orm->getModel($params['entity']); +if(!$entityInstance) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$method_name = 'getPolicies' ; +$class = new ReflectionClass($entityInstance::getType()); +if(!($class->getMethod($method_name)->class == $entityInstance::getType())) { + equal::run('do','core_config_create-policies',['entity' => $entity->getFullName()], true); +} + +$entity->updateMethod($method_name,$params['payload']); + +$context->httpResponse() + ->status(204) + ->send(); + diff --git a/packages/core/actions/config/update-roles.php b/packages/core/actions/config/update-roles.php new file mode 100644 index 00000000..679da266 --- /dev/null +++ b/packages/core/actions/config/update-roles.php @@ -0,0 +1,55 @@ + "Update the getRoles() return function of an entity", + 'help' => "This controller relies on the PHP binary. Ensure the PHP binary is present in the PATH.", + 'params' => [ + 'entity' => [ + 'description' => 'Name of the entity (class).', + 'type' => 'string', + 'required' => true + ], + 'payload' => [ + 'description' => 'Actions definition.', + 'type' => 'array', + 'required' => false + ] + ], + 'response' => [ + 'content-type' => 'text/plain', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ + +list($context, $orm) = [$providers['context'], $providers['orm']]; + + +$entity = new Entity($params['entity']); + +// force class autoload +$entityInstance = $orm->getModel($params['entity']); +if(!$entityInstance) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$method_name = 'getRoles' ; +$class = new ReflectionClass($entityInstance::getType()); +if(!($class->getMethod($method_name)->class == $entityInstance::getType())) { + equal::run('do','core_config_create-roles',['entity' => $entity->getFullName()], true); +} + + +$entity->updateMethod($method_name,$params['payload']); + +$context->httpResponse() + ->status(204) + ->send(); + diff --git a/packages/core/data/model/pipeline-load.php b/packages/core/data/model/pipeline-load.php new file mode 100644 index 00000000..9e9c6108 --- /dev/null +++ b/packages/core/data/model/pipeline-load.php @@ -0,0 +1,126 @@ + "Load a full pipeline with its nodes, metadata, links and parameters.", + 'params' => [ + 'name' => [ + 'description' => "Pipeline name", + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'utf-8', + 'accept-origin' => '*' + ], + 'providers' => ['context'] +]); + +/** @var Context $context */ +$context = $providers['context']; + +// Step 1: Get pipeline by name +$pipelineList = \eQual::run('get', 'model_collect', [ + 'entity' => 'core\\pipeline\\Pipeline', + 'domain' => [['name', '=', $params['name']]], + 'fields' => ['id', 'name', 'nodes_ids'], + 'nolimit' => true +], true); + +if (count($pipelineList) === 0) { + throw new \Exception("pipeline_not_found"); +} + +$pipeline = $pipelineList[0]; +$nodes_ids = $pipeline['nodes_ids'] ?? []; + +$result = [ + 'pipeline' => [ + 'id' => $pipeline['id'], + 'name' => $pipeline['name'] + ], + 'nodes' => [], + 'links' => [], + 'parameters' => [] +]; + +// Step 2: Load all nodes +$nodes = \eQual::run('get', 'model_collect', [ + 'entity' => 'core\\pipeline\\Node', + 'domain' => [['id', 'in', $nodes_ids]], + 'fields' => [ + 'id', 'name', 'description', + 'out_links_ids', 'in_links_ids', + 'operation_controller', 'operation_type', + 'params_ids' + ], + 'nolimit' => true +], true); + +// Step 3: Load Meta for nodes +$metaList = \eQual::run('get', 'model_collect', [ + 'entity' => 'core\\Meta', + 'domain' => [['reference', 'in', array_map(fn($id) => 'node.' . $id, $nodes_ids)]], + 'fields' => ['reference', 'value'], + 'nolimit' => true +], true); + +$metaMap = []; +foreach ($metaList as $meta) { + $nodeId = (int) str_replace('node.', '', $meta['reference']); + $metaMap[$nodeId] = json_decode($meta['value'], true); +} + +$linkIds = []; +$paramIds = []; + +// Step 4: Process nodes and collect link/param IDs +foreach ($nodes as $node) { + $nodeId = $node['id']; + + $result['nodes'][] = [ + ...$node, + 'meta' => $metaMap[$nodeId] ?? ['position' => ['x' => 0, 'y' => 0], 'icon' => '', 'color' => ''] + ]; + + $linkIds = array_merge($linkIds, $node['in_links_ids'], $node['out_links_ids']); + $paramIds = array_merge($paramIds, $node['params_ids']); +} + +// Step 5: Load links +$linkIds = array_unique($linkIds); +if (!empty($linkIds)) { + $links = \eQual::run('get', 'model_collect', [ + 'entity' => 'core\\pipeline\\NodeLink', + 'domain' => [['id', 'in', $linkIds]], + 'fields' => ['id', 'reference_node_id', 'source_node_id', 'target_node_id', 'target_param'], + 'nolimit' => true + ], true); + $result['links'] = $links; +} + +// Step 6: Load parameters +$paramIds = array_unique($paramIds); +if (!empty($paramIds)) { + $params = \eQual::run('get', 'model_collect', [ + 'entity' => 'core\\pipeline\\Parameter', + 'domain' => [['id', 'in', $paramIds]], + 'fields' => ['id', 'node_id', 'param', 'value'], + 'nolimit' => true + ], true); + + // Decode values + foreach ($params as &$p) { + $p['value'] = json_decode($p['value'], true); + } + + $result['parameters'] = $params; +} + +$context->httpResponse() + ->body($result) + ->send(); \ No newline at end of file diff --git a/packages/core/data/run-pipeline.php b/packages/core/data/run-pipeline.php index 06a5be8a..e297a13d 100644 --- a/packages/core/data/run-pipeline.php +++ b/packages/core/data/run-pipeline.php @@ -67,8 +67,21 @@ foreach ($node['params_ids'] as $param) { $parameters[$param['param']] = json_decode($param['value']); } - $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); - $count++; + try { + $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); + $count++; + } catch (Exception $e) { + // Retourner les résultats partiels et l'erreur + $context->httpResponse() + ->status(500) + ->body([ + 'error' => 'Pipeline execution failed at node: ' . $node['name'], + 'message' => $e->getMessage(), + 'partial_results' => $result_map + ]) + ->send(); + return; + } } } } @@ -90,8 +103,20 @@ foreach ($node['params_ids'] as $param) { $parameters[$param['param']] = json_decode($param['value']); } - $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); - $count++; + try { + $result_map[$node['id']] = eQual::run($node['operation_type'], $node['operation_controller'], $parameters, true); + $count++; + } catch (Exception $e) { + $context->httpResponse() + ->status(200) + ->body([ + - 'error' => 'Pipeline execution failed at node: ' . $node['name'], + 'message' => $e->getMessage(), + 'partial_results' => $result_map + ]) + ->send(); + return; + } } } } @@ -106,3 +131,4 @@ $context->httpResponse() ->body($res) ->send(); +?>