diff --git a/lib/Db/AuditTrailMapper.php b/lib/Db/AuditTrailMapper.php index aa103fd933..c62b2f6cd1 100644 --- a/lib/Db/AuditTrailMapper.php +++ b/lib/Db/AuditTrailMapper.php @@ -8,14 +8,35 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +use OCA\OpenRegister\Db\ObjectEntityMapper; +/** + * The AuditTrailMapper class + * + * @package OCA\OpenRegister\Db + */ class AuditTrailMapper extends QBMapper { - public function __construct(IDBConnection $db) + private $objectEntityMapper; + + /** + * Constructor for the AuditTrailMapper + * + * @param IDBConnection $db The database connection + * @param ObjectEntityMapper $objectEntityMapper The object entity mapper + */ + public function __construct(IDBConnection $db, ObjectEntityMapper $objectEntityMapper) { parent::__construct($db, 'openregister_audit_trails'); + $this->objectEntityMapper = $objectEntityMapper; } + /** + * Finds an audit trail by id + * + * @param int $id The id of the audit trail + * @return Log The audit trail + */ public function find(int $id): Log { $qb = $this->db->getQueryBuilder(); @@ -29,6 +50,16 @@ public function find(int $id): Log return $this->findEntity(query: $qb); } + /** + * Finds all audit trails + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The audit trails + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -58,6 +89,30 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Finds all audit trails for a given object + * + * @param string $idOrUuid The id or uuid of the object + * @return array The audit trails + */ + public function findAllUuid(string $idOrUuid, ?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + try { + $object = $this->objectEntityMapper->find(idOrUuid: $idOrUuid); + $objectId = $object->getId(); + $filters['object'] = $objectId; + return $this->findAll($limit, $offset, $filters, $searchConditions, $searchParams); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return []; + } + } + + /** + * Creates an audit trail from an array + * + * @param array $object The object to create the audit trail from + * @return Log The created audit trail + */ public function createFromArray(array $object): Log { $log = new Log(); diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 9d1accf980..a883fba1bb 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -14,12 +14,23 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The ObjectEntityMapper class + * + * @package OCA\OpenRegister\Db + */ class ObjectEntityMapper extends QBMapper { private IDatabaseJsonService $databaseJsonService; public const MAIN_FILTERS = ['register', 'schema', 'uuid', 'created', 'updated']; + /** + * Constructor for the ObjectEntityMapper + * + * @param IDBConnection $db The database connection + * @param MySQLJsonService $mySQLJsonService The MySQL JSON service + */ public function __construct(IDBConnection $db, MySQLJsonService $mySQLJsonService) { parent::__construct($db, 'openregister_objects'); @@ -103,6 +114,13 @@ public function findByRegisterAndSchema(string $register, string $schema): Objec return $this->findEntities(query: $qb); } + /** + * Counts all objects + * + * @param array|null $filters The filters to apply + * @param string|null $search The search string to apply + * @return int The number of objects + */ public function countAll(?array $filters = [], ?string $search = null): int { $qb = $this->db->getQueryBuilder(); @@ -172,6 +190,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates an object from an array + * + * @param array $object The object to create + * @return ObjectEntity The created object + */ public function createFromArray(array $object): ObjectEntity { $obj = new ObjectEntity(); @@ -182,6 +206,13 @@ public function createFromArray(array $object): ObjectEntity return $this->insert($obj); } + /** + * Updates an object from an array + * + * @param int $id The id of the object to update + * @param array $object The object to update + * @return ObjectEntity The updated object + */ public function updateFromArray(int $id, array $object): ObjectEntity { $obj = $this->find($id); @@ -195,6 +226,13 @@ public function updateFromArray(int $id, array $object): ObjectEntity return $this->update($obj); } + /** + * Gets the facets for the objects + * + * @param array $filters The filters to apply + * @param string|null $search The search string to apply + * @return array The facets + */ public function getFacets(array $filters = [], ?string $search = null) { if(key_exists(key: 'register', array: $filters) === true) { diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index 475c6df882..9dcdf83a5c 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -11,6 +11,11 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The RegisterMapper class + * + * @package OCA\OpenRegister\Db + */ class RegisterMapper extends QBMapper { private $schemaMapper; diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index d1da76e9c3..314ed550ed 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -72,33 +72,38 @@ public function hydrate(array $object): self return $this; } - + /** + * Serializes the schema to an array + * + * @return array + */ public function jsonSerialize(): array { $properties = []; - foreach ($this->properties as $key => $property) { - $properties[$key] = $property; - if (isset($property['type']) === false) { - $properties[$key] = $property; - continue; - } - switch ($property['format']) { - case 'string': - // For now array as string - case 'array': - $properties[$key]['default'] = (string) $property; - break; - case 'int': - case 'integer': - case 'number': - $properties[$key]['default'] = (int) $property; - break; - case 'bool': - $properties[$key]['default'] = (bool) $property; - break; - - } - } + if (isset($this->properties) === true) { + foreach ($this->properties as $key => $property) { + $properties[$key] = $property; + if (isset($property['type']) === false) { + $properties[$key] = $property; + continue; + } + switch ($property['format']) { + case 'string': + // For now array as string + case 'array': + $properties[$key]['default'] = (string) $property; + break; + case 'int': + case 'integer': + case 'number': + $properties[$key]['default'] = (int) $property; + break; + case 'bool': + $properties[$key]['default'] = (bool) $property; + break; + } + } + } $array = [ 'id' => $this->id, diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 0af71f428d..2ca6725f80 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -9,13 +9,29 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The SchemaMapper class + * + * @package OCA\OpenRegister\Db + */ class SchemaMapper extends QBMapper { + /** + * Constructor for the SchemaMapper + * + * @param IDBConnection $db The database connection + */ public function __construct(IDBConnection $db) { parent::__construct($db, 'openregister_schemas'); } + /** + * Finds a schema by id + * + * @param int $id The id of the schema + * @return Schema The schema + */ public function find(int $id): Schema { $qb = $this->db->getQueryBuilder(); @@ -28,6 +44,13 @@ public function find(int $id): Schema return $this->findEntity(query: $qb); } + + /** + * Finds multiple schemas by id + * + * @param array $ids The ids of the schemas + * @return array The schemas + */ public function findMultiple(array $ids): array { $result = []; @@ -38,6 +61,17 @@ public function findMultiple(array $ids): array return $result; } + + /** + * Finds all schemas + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The schemas + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -67,6 +101,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates a schema from an array + * + * @param array $object The object to create + * @return Schema The created schema + */ public function createFromArray(array $object): Schema { $schema = new Schema(); @@ -80,6 +120,13 @@ public function createFromArray(array $object): Schema return $this->insert(entity: $schema); } + /** + * Updates a schema from an array + * + * @param int $id The id of the schema to update + * @param array $object The object to update + * @return Schema The updated schema + */ public function updateFromArray(int $id, array $object): Schema { $obj = $this->find($id); diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index 9916cb0f1c..3dbfac3774 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -9,13 +9,30 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The SourceMapper class + * + * @package OCA\OpenRegister\Db + */ class SourceMapper extends QBMapper { + /** + * Constructor for the SourceMapper + * + * @param IDBConnection $db The database connection + */ public function __construct(IDBConnection $db) { parent::__construct($db, 'openregister_sources'); } + + /** + * Finds a source by id + * + * @param int $id The id of the source + * @return Source The source + */ public function find(int $id): Source { $qb = $this->db->getQueryBuilder(); @@ -29,6 +46,16 @@ public function find(int $id): Source return $this->findEntity(query: $qb); } + /** + * Finds all sources + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The sources + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -58,6 +85,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates a source from an array + * + * @param array $object The object to create + * @return Source The created source + */ public function createFromArray(array $object): Source { $source = new Source(); @@ -70,6 +103,13 @@ public function createFromArray(array $object): Source return $this->insert(entity: $source); } + /** + * Updates a source from an array + * + * @param int $id The id of the source to update + * @param array $object The object to update + * @return Source The updated source + */ public function updateFromArray(int $id, array $object): Source { $obj = $this->find($id); diff --git a/lib/Service/MongoDbService.php b/lib/Service/MongoDbService.php index 1e307dea2e..74bb04ac16 100755 --- a/lib/Service/MongoDbService.php +++ b/lib/Service/MongoDbService.php @@ -7,22 +7,34 @@ use GuzzleHttp\Exception\ClientException; use Symfony\Component\Uid\Uuid; +/** + * Service class for handling MongoDB operations + * + * This class provides methods for interacting with MongoDB through a REST API, + * including CRUD operations, aggregations, and search functionality. + * It handles configuration, connection management and data transformation. + */ class MongoDbService { - + /** + * Default base configuration for MongoDB operations + * + * @var array + */ public const BASE_OBJECT = [ - 'database' => 'objects', - 'collection' => 'json', + 'database' => 'objects', // The default database name + 'collection' => 'json', // The default collection name ]; /** - * Gets a guzzle client based upon given config. + * Gets a configured Guzzle HTTP client * - * @param array $config The config to be used for the client. - * @return Client + * @param array $config Configuration array containing connection details + * @return Client Configured Guzzle client instance */ public function getClient(array $config): Client { + // Remove MongoDB specific config before creating Guzzle client $guzzleConf = $config; unset($guzzleConf['mongodbCluster']); @@ -32,21 +44,24 @@ public function getClient(array $config): Client /** * Save an object to MongoDB * - * @param array $data The data to be saved. - * @param array $config The configuration that should be used by the call. - * - * @return array The resulting object. - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $data The data object to be saved + * @param array $config MongoDB connection configuration + * @return array The saved object with generated ID + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function saveObject(array $data, array $config): array { + // Initialize HTTP client $client = $this->getClient(config: $config); + // Prepare object with base configuration and data $object = self::BASE_OBJECT; $object['dataSource'] = $config['mongodbCluster']; $object['document'] = $data; + // Generate and set UUID for new document $object['document']['id'] = $object['document']['_id'] = Uuid::v4(); + // Insert document via API $result = $client->post( uri: 'action/insertOne', options: ['json' => $object], @@ -57,23 +72,23 @@ public function saveObject(array $data, array $config): array ); $id = $resultData['insertedId']; + // Return complete object by finding it with new ID return $this->findObject(filters: ['_id' => $id], config: $config); } /** - * Finds objects based upon a set of filters. - * - * @param array $filters The filters to compare the object to. - * @param array $config The configuration that should be used by the call. + * Find multiple objects matching given filters * - * @return array The objects found for given filters. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match documents + * @param array $config MongoDB connection configuration + * @return array Array of matching documents + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function findObjects(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare query object $object = self::BASE_OBJECT; $object['dataSource'] = $config['mongodbCluster']; $object['filter'] = $filters; @@ -83,6 +98,7 @@ public function findObjects(array $filters, array $config): array // $object['filter'][] = ['$sort' => $sort]; // } + // Execute find query via API $returnData = $client->post( uri: 'action/find', options: ['json' => $object] @@ -95,23 +111,23 @@ public function findObjects(array $filters, array $config): array } /** - * Finds an object based upon a set of filters (usually the id) - * - * @param array $filters The filters to compare the objects to. - * @param array $config The config to be used by the call. + * Find a single object matching given filters * - * @return array The resulting object. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document + * @param array $config MongoDB connection configuration + * @return array The matched document + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function findObject(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare query object $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['dataSource'] = $config['mongodbCluster']; + // Execute findOne query via API $returnData = $client->post( uri: 'action/findOne', options: ['json' => $object] @@ -125,59 +141,57 @@ public function findObject(array $filters, array $config): array return $result['document']; } - - /** - * Updates an object in MongoDB - * - * @param array $filters The filter to search the object with (id) - * @param array $update The fields that should be updated. - * @param array $config The configuration to be used by the call. - * - * @return array The updated object. + * Update an existing object in MongoDB * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document for update + * @param array $update Update operations to apply + * @param array $config MongoDB connection configuration + * @return array The updated document + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function updateObject(array $filters, array $update, array $config): array { $client = $this->getClient(config: $config); + // Convert update data to dot notation for nested updates $dotUpdate = new Dot($update); + // Prepare update query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['update']['$set'] = $update; $object['upsert'] = true; $object['dataSource'] = $config['mongodbCluster']; + // Execute update via API + $returnData = $client->post( + uri: 'action/updateOne', + options: ['json' => $object] + ); - - $returnData = $client->post( - uri: 'action/updateOne', - options: ['json' => $object] - ); - + // Return updated document return $this->findObject($filters, $config); } /** - * Delete an object according to a filter (id specifically) + * Delete an object from MongoDB * - * @param array $filters The filters to use. - * @param array $config The config to be used by the call. - * - * @return array An empty array. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document for deletion + * @param array $config MongoDB connection configuration + * @return array Empty array on successful deletion + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function deleteObject(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare delete query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['dataSource'] = $config['mongodbCluster']; + // Execute deletion via API $returnData = $client->post( uri: 'action/deleteOne', options: ['json' => $object] @@ -187,23 +201,25 @@ public function deleteObject(array $filters, array $config): array } /** - * Aggregates objects for search facets. + * Perform aggregation operations on MongoDB collection * - * @param array $filters The filters apply to the search request. - * @param array $pipeline The pipeline to use. - * @param array $config The configuration to use in the call. - * @return array - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Initial query filters + * @param array $pipeline Aggregation pipeline stages + * @param array $config MongoDB connection configuration + * @return array Aggregation results + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function aggregateObjects(array $filters, array $pipeline, array $config):array { $client = $this->getClient(config: $config); + // Prepare aggregation query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['pipeline'] = $pipeline; $object['dataSource'] = $config['mongodbCluster']; + // Execute aggregation via API $returnData = $client->post( uri: 'action/aggregate', options: ['json' => $object] @@ -213,7 +229,5 @@ public function aggregateObjects(array $filters, array $pipeline, array $config) json: $returnData->getBody()->getContents(), associative: true ); - } - } diff --git a/lib/Service/MySQLJsonService.php b/lib/Service/MySQLJsonService.php index 43e9150144..f0ff964f02 100644 --- a/lib/Service/MySQLJsonService.php +++ b/lib/Service/MySQLJsonService.php @@ -5,18 +5,30 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; +/** + * Service class for handling MySQL JSON operations + * + * This class provides methods for querying and filtering JSON data stored in MySQL, + * including complex filtering, searching, ordering and aggregation functionality. + */ class MySQLJsonService implements IDatabaseJsonService { /** - * @inheritDoc + * Add ordering to a query based on JSON fields + * + * @param IQueryBuilder $builder The query builder instance + * @param array $order Array of field => direction pairs for ordering + * @return IQueryBuilder The modified query builder */ public function orderJson(IQueryBuilder $builder, array $order = []): IQueryBuilder { - + // Loop through each ordering field and direction foreach($order as $item=>$direction) { + // Create parameters for the JSON path and sort direction $builder->createNamedParameter(value: "$.$item", placeHolder: ":path$item"); $builder->createNamedParameter(value: $direction, placeHolder: ":direction$item"); + // Add ORDER BY clause using JSON_UNQUOTE and JSON_EXTRACT $builder->orderBy($builder->createFunction("json_unquote(json_extract(object, :path$item))"),$direction); } @@ -24,12 +36,18 @@ public function orderJson(IQueryBuilder $builder, array $order = []): IQueryBuil } /** - * @inheritDoc + * Add full-text search functionality for JSON fields + * + * @param IQueryBuilder $builder The query builder instance + * @param string|null $search The search term to look for + * @return IQueryBuilder The modified query builder */ public function searchJson(IQueryBuilder $builder, ?string $search = null): IQueryBuilder { if ($search !== null) { + // Create parameter for the search term with wildcards $builder->createNamedParameter(value: "%$search%", placeHolder: ':search'); + // Add WHERE clause to search case-insensitive across all JSON fields $builder->andWhere("JSON_SEARCH(LOWER(object), 'one', LOWER(:search)) IS NOT NULL"); } @@ -39,28 +57,33 @@ public function searchJson(IQueryBuilder $builder, ?string $search = null): IQue /** * Add complex filters to the filter set. * - * @param IQueryBuilder $builder The query builder - * @param string $filter The filtered field. - * @param array $values The values to filter on. + * Handles special filter cases like 'after' and 'before' for date ranges, + * as well as IN clauses for arrays of values. * - * @return IQueryBuilder The updated query builder. + * @param IQueryBuilder $builder The query builder instance + * @param string $filter The filtered field + * @param array $values The values to filter on + * @return IQueryBuilder The modified query builder */ private function jsonFilterArray(IQueryBuilder $builder, string $filter, array $values): IQueryBuilder { foreach ($values as $key=>$value) { switch ($key) { case 'after': + // Add >= filter for dates after specified value $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value{$filter}after"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) >= (:value{$filter}after)"); break; case 'before': - $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value${filter}before"); + // Add <= filter for dates before specified value + $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value{$filter}before"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) <= (:value{$filter}before)"); break; default: - $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value$filter"); + // Add IN clause for array of values + $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value{$filter}"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) IN (:value$filter)"); break; @@ -74,18 +97,22 @@ private function jsonFilterArray(IQueryBuilder $builder, string $filter, array $ /** * Build a string to search multiple values in an array. * - * @param array $values The values to search for. - * @param string $filter The field to filter on. - * @param IQueryBuilder $builder The query builder. + * Creates an OR condition for each value to check if it exists + * within a JSON array field. * - * @return string The resulting query string. + * @param array $values The values to search for + * @param string $filter The field to filter on + * @param IQueryBuilder $builder The query builder instance + * @return string The resulting OR conditions as a string */ private function getMultipleContains (array $values, string $filter, IQueryBuilder $builder): string { $orString = ''; foreach($values as $key=>$value) { + // Create parameter for each value $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value$filter$key"); + // Add OR condition checking if value exists in JSON array $orString .= " OR json_contains(object, json_quote(:value$filter$key), :path$filter)"; } @@ -93,27 +120,39 @@ private function getMultipleContains (array $values, string $filter, IQueryBuild } /** - * @inheritDoc + * Add JSON filtering to a query + * + * Handles various filter types including: + * - Complex filters (after/before) + * - Array filters + * - Simple equality filters + * + * @param IQueryBuilder $builder The query builder instance + * @param array $filters Array of filters to apply + * @return IQueryBuilder The modified query builder */ public function filterJson(IQueryBuilder $builder, array $filters): IQueryBuilder { + // Remove special system fields from filters unset($filters['register'], $filters['schema'], $filters['updated'], $filters['created'], $filters['_queries']); foreach($filters as $filter=>$value) { - + // Create parameter for JSON path $builder->createNamedParameter(value: "$.$filter", placeHolder: ":path$filter"); if(is_array($value) === true && array_is_list($value) === false) { + // Handle complex filters (after/before) $builder = $this->jsonFilterArray(builder: $builder, filter: $filter, values: $value); continue; } else if (is_array($value) === true) { - + // Handle array of values with IN clause and contains check $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value$filter"); $builder ->andWhere("(json_unquote(json_extract(object, :path$filter)) IN (:value$filter))". $this->getMultipleContains($value, $filter, $builder)); continue; } + // Handle simple equality filter $builder->createNamedParameter(value: $value, placeHolder: ":value$filter"); $builder ->andWhere("json_extract(object, :path$filter) = :value$filter OR json_contains(object, json_quote(:value$filter), :path$filter)"); @@ -123,16 +162,28 @@ public function filterJson(IQueryBuilder $builder, array $filters): IQueryBuilde } /** - * @inheritDoc + * Get aggregations (facets) for specified fields + * + * Returns counts of unique values for each specified field, + * filtered by the provided filters and search term. + * + * @param IQueryBuilder $builder The query builder instance + * @param array $fields Fields to get aggregations for + * @param int $register Register ID to filter by + * @param int $schema Schema ID to filter by + * @param array $filters Additional filters to apply + * @param string|null $search Optional search term + * @return array Array of facets with counts for each field */ public function getAggregations(IQueryBuilder $builder, array $fields, int $register, int $schema, array $filters = [], ?string $search = null): array { $facets = []; foreach($fields as $field) { + // Create parameter for JSON path $builder->createNamedParameter(value: "$.$field", placeHolder: ":$field"); - + // Build base query for aggregation $builder ->selectAlias($builder->createFunction("json_unquote(json_extract(object, :$field))"), '_id') ->selectAlias($builder->createFunction("count(*)"), 'count') @@ -143,12 +194,15 @@ public function getAggregations(IQueryBuilder $builder, array $fields, int $regi ) ->groupBy('_id'); + // Apply filters and search $builder = $this->filterJson($builder, $filters); $builder = $this->searchJson($builder, $search); + // Execute query and store results $result = $builder->executeQuery(); $facets[$field] = $result->fetchAll(); + // Reset builder for next field $builder->resetQueryParts(); $builder->setParameters([]); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 4d2513821b..90d51133dc 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -22,467 +22,558 @@ use Symfony\Component\Uid\Uuid; use GuzzleHttp\Client; +/** + * Service class for handling object operations + * + * This service provides methods for CRUD operations on objects, including: + * - Creating, reading, updating and deleting objects + * - Finding objects by ID/UUID + * - Getting audit trails + * - Extending objects with related data + * + * @package OCA\OpenRegister\Service + */ class ObjectService { - - private int $register; - private int $schema; - - private AuditTrailMapper $auditTrailMapper; - - /** - * The constructor sets al needed variables. - * - * @param ObjectEntityMapper $objectEntityMapper The ObjectEntity Mapper - */ - public function __construct( - ObjectEntityMapper $objectEntityMapper, - RegisterMapper $registerMapper, - SchemaMapper $schemaMapper, - AuditTrailMapper $auditTrailMapper, - private readonly IURLGenerator $urlGenerator, - ) - { - $this->objectEntityMapper = $objectEntityMapper; - $this->registerMapper = $registerMapper; - $this->schemaMapper = $schemaMapper; - $this->auditTrailMapper = $auditTrailMapper; - } - - public function find(int|string $id) { - return $this->getObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $id - ); - } - - public function createFromArray(array $object) { - return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } - - public function updateFromArray(string $id, array $object, bool $updatedObject) { - $object['id'] = $id; - - return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } - - public function delete(array|\JsonSerializable $object): bool - { - if($object instanceof \JsonSerializable === true) { - $object = $object->jsonSerialize(); - } - - return $this->deleteObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $object['id'] - ); - } - - public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array - { - $objects = $this->getObjects( - register: $this->getRegister(), - schema: $this->getSchema(), - limit: $limit, - offset: $offset, - filters: $filters, - sort: $sort, - search: $search - ); -// $data = array_map([$this, 'getDataFromObject'], $objects); - - return $objects; - } - - public function count(array $filters = [], ?string $search = null): int - { - if($this->getSchema() !== null && $this->getRegister() !== null) { - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); - } - $count = $this->objectEntityMapper - ->countAll(filters: $filters, search: $search); - - return $count; - } - - public function findMultiple(array $ids): array - { - $result = []; - foreach($ids as $id) { - $result[] = $this->find($id); - } - - return $result; - } - - public function getAggregations(array $filters, ?string $search = null): array - { - $mapper = $this->getMapper(objectType: 'objectEntity'); - - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); - - if ($mapper instanceof ObjectEntityMapper === true) { - $facets = $this->objectEntityMapper->getFacets($filters, $search); - return $facets; - } - - return []; - } - - private function getDataFromObject(mixed $object) { - - return $object->getObject(); - } - - /** - * Gets all objects of a specific type. - * - * @param string|null $objectType The type of objects to retrieve. - * @param int|null $register - * @param int|null $schema - * @param int|null $limit The maximum number of objects to retrieve. - * @param int|null $offset The offset from which to start retrieving objects. - * @param array $filters - * @return array The retrieved objects. - * @throws \Exception - */ - public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array - { - if($objectType === null && $register !== null && $schema !== null) { - $objectType = 'objectEntity'; - $filters['register'] = $register; - $filters['schema'] = $schema; - } - - // Get the appropriate mapper for the object type - $mapper = $this->getMapper($objectType); - - // Use the mapper to find and return all objects of the specified type - return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); - } - - /** - * Validate an object with a schema. - * If schema is not given and schemaObject is filled, the object will validate to the schemaObject. - * - * @param array $object The object to validate. - * @param int|null $schema The id of the schema to validate to. - * @param object $schemaObject A schema object to validate to. - * - * @return ValidationResult The validation result from opis/json-schema. - */ - public function validateObject(array $object, ?int $schema = null, object $schemaObject = new stdClass()): ValidationResult - { - if ($schemaObject === new stdClass() || $schema !== null) { - $schemaObject = $this->schemaMapper->find($schema)->getSchemaObject($this->urlGenerator); - } - - $validator = new Validator(); - $validator->setMaxErrors(100); - $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); - - return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); - - } - - /** - * Save an object - * - * @param Register|string $register The register to save the object to. - * @param Schema|string $schema The schema to save the object to. - * @param array $object The data to be saved. - * - * @return ObjectEntity The resulting object. - */ - public function saveObject(int $register, int $schema, array $object): ObjectEntity - { - - if (isset($object['id']) === true) { - // Does the object already exist? - $objectEntity = $this->objectEntityMapper->findByUuid($this->registerMapper->find($register), $this->schemaMapper->find($schema), $object['id']); - } - - $validationResult = $this->validateObject(object: $object, schema: $schema); - - if ($objectEntity === null){ - $objectEntity = new ObjectEntity(); - $objectEntity->setRegister($register); - $objectEntity->setSchema($schema); - ///return $this->objectEntityMapper->update($objectEntity); - } - // Does the object have an if? - if (isset($object['id']) && !empty($object['id'])) { - // Update existing object - $objectEntity->setUuid($object['id']); - } else { - // Create new object - $objectEntity->setUuid(Uuid::v4()); - $object['id'] = $objectEntity->getUuid(); - } - - $oldObject = $objectEntity->getObject(); - $objectEntity->setObject($object); - - // If the object has no uuid, create a new one - if (empty($objectEntity->getUuid())) { - $objectEntity->setUuid(Uuid::v4()); - } - - $schemaObject = $this->schemaMapper->find($schema); - - if ($objectEntity->getId() && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ - $objectEntity = $this->objectEntityMapper->update($objectEntity); - $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); - } - else if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { - $objectEntity = $this->objectEntityMapper->insert($objectEntity); - $this->auditTrailMapper->createAuditTrail(new: $objectEntity); - } - - if ($validationResult->isValid() === false) { - throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); - } - - return $objectEntity; - } - - - /** - * Get an object - * - * @param Register $register The register to save the object to. - * @param string $uuid The uuid of the object to get - * - * @return ObjectEntity The resulting object. - */ - public function getObject(Register $register, Schema $schema, string $uuid): ObjectEntity - { - - // Lets see if we need to save to an internal source - if ($register->getSource() === 'internal' || $register->getSource() === '') { - return $this->objectEntityMapper->findByUuid($register, $schema, $uuid); - } - - //@todo mongodb support - - // Handle external source here if needed - throw new \Exception('Unsupported source type'); - } - - /** - * Delete an object - * - * @param Register $register The register to delete the object from. - * @param string $uuid The uuid of the object to delete - - * @return ObjectEntity The resulting object. - */ - public function deleteObject(Register $register, Schema $schema, string $uuid): bool - { - // Lets see if we need to save to an internal source - if ($register->getSource() === 'internal' || $register->getSource() === '') { - $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); - $this->objectEntityMapper->delete($object); - - return true; - } - - //@todo mongodb support - - // Handle external source here if needed - throw new \Exception('Unsupported source type'); - } - - /** - * Gets the appropriate mapper based on the object type. - * - * @param string $objectType The type of object to retrieve the mapper for. - * @return mixed The appropriate mapper. - * @throws \InvalidArgumentException If an unknown object type is provided. - * @throws \Exception If OpenRegister service is not available or if register/schema is not configured. - */ - public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null) - { - if($register !== null && $schema !== null) { - $this->setSchema($schema); - $this->setRegister($register); - - return $this; - } - - // If the source is internal, return the appropriate mapper based on the object type - switch ($objectType) { - case 'register': - return $this->registerMapper; - case 'schema': - return $this->schemaMapper; - case 'objectEntity': - return $this->objectEntityMapper; - default: - throw new \InvalidArgumentException("Unknown object type: $objectType"); - } - } - - - - /** - * Gets multiple objects based on the object type and ids. - * - * @param string $objectType The type of objects to retrieve. - * @param array $ids The ids of the objects to retrieve. - * @return array The retrieved objects. - * @throws \InvalidArgumentException If an unknown object type is provided. - */ - public function getMultipleObjects(string $objectType, array $ids) - { - // Process the ids - $processedIds = array_map(function($id) { - if (is_object($id) && method_exists($id, 'getId')) { - return $id->getId(); - } elseif (is_array($id) && isset($id['id'])) { - return $id['id']; - } else { - return $id; - } - }, $ids); - - // Clean up the ids if they are URIs - $cleanedIds = array_map(function($id) { - // If the id is a URI, get only the last part of the path - if (filter_var($id, FILTER_VALIDATE_URL)) { - $parts = explode('/', rtrim($id, '/')); - return end($parts); - } - return $id; - }, $processedIds); - - // Get the appropriate mapper for the object type - $mapper = $this->getMapper($objectType); - - // Use the mapper to find and return multiple objects based on the provided cleaned ids - return $mapper->findMultiple($cleanedIds); - } - - /** - * Extends an entity with related objects based on the extend array. - * - * @param mixed $entity The entity to extend - * @param array $extend An array of properties to extend - * @return array The extended entity as an array - * @throws \Exception If a property is not present on the entity - */ - public function extendEntity(array $entity, array $extend): array - { - // Convert the entity to an array if it's not already one - if(is_array($entity)) { - $result = $entity; - } else { - $result = $entity->jsonSerialize(); - } - - // Iterate through each property to be extended - foreach ($extend as $property) { - // Create a singular property name - $singularProperty = rtrim($property, 's'); - - // Check if property or singular property are keys in the array - if (array_key_exists(key: $property, array: $result) === true) { - $value = $result[$property]; - if (empty($value)) { - continue; - } - } elseif (array_key_exists(key: $singularProperty, array: $result)) { - $value = $result[$singularProperty]; - } else { - throw new \Exception("Property '$property' or '$singularProperty' is not present in the entity."); - } - - // Get a mapper for the property - $propertyObject = $property; - try { - $mapper = $this->getMapper(objectType: $property); - $propertyObject = $singularProperty; - } catch (\Exception $e) { - try { - $mapper = $this->getMapper(objectType: $singularProperty); - $propertyObject = $singularProperty; - } catch (\Exception $e) { - // If still no mapper, throw a no mapper available error - throw new \Exception(message: "No mapper available for property '$property'."); - } - } - - // Update the values - if (is_array($value) === true) { - // If the value is an array, get multiple related objects - $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value); - } else { - // If the value is not an array, get a single related object - $objectId = is_object(value: $value) ? $value->getId() : $value; - $result[$property] = $mapper->find($objectId); - } - } - - // Return the extended entity as an array - return $result; - } - - /** - * Get the registers extended with schemas for this instance of OpenRegisters - * - * @return array The registers of this OpenRegisters instance extended with schemas - * @throws \Exception - */ - public function getRegisters(): array - { - $registers = $this->registerMapper->findAll(); - - - // Convert entity objects to arrays using jsonSerialize - $registers = array_map(function($object) { - return $object->jsonSerialize(); - }, $registers); - - $extend = ['schemas']; - // Extend the objects if the extend array is not empty - if(empty($extend) === false) { - $registers = array_map(function($object) use ($extend) { - return $this->extendEntity(entity: $object, extend: $extend); - }, $registers); - } - - return $registers; - } - - public function getRegister(): int - { - return $this->register; - } - - public function setRegister(int $register): void - { - $this->register = $register; - } - - public function getSchema(): int - { - return $this->schema; - } - - public function setSchema(int $schema): void - { - $this->schema = $schema; - } + /** @var int The current register ID */ + private int $register; + + /** @var int The current schema ID */ + private int $schema; + + /** @var AuditTrailMapper For tracking object changes */ + private AuditTrailMapper $auditTrailMapper; + + /** + * Constructor for ObjectService + * + * Initializes the service with required mappers for database operations + * + * @param ObjectEntityMapper $objectEntityMapper Mapper for object entities + * @param RegisterMapper $registerMapper Mapper for registers + * @param SchemaMapper $schemaMapper Mapper for schemas + * @param AuditTrailMapper $auditTrailMapper Mapper for audit trails + */ + public function __construct( + ObjectEntityMapper $objectEntityMapper, + RegisterMapper $registerMapper, + SchemaMapper $schemaMapper, + AuditTrailMapper $auditTrailMapper + ) + { + $this->objectEntityMapper = $objectEntityMapper; + $this->registerMapper = $registerMapper; + $this->schemaMapper = $schemaMapper; + $this->auditTrailMapper = $auditTrailMapper; + } + + /** + * Find an object by ID or UUID + * + * @param int|string $id The ID or UUID to search for + * @return ObjectEntity The found object + */ + public function find(int|string $id) { + return $this->getObject( + register: $this->registerMapper->find($this->getRegister()), + schema: $this->schemaMapper->find($this->getSchema()), + uuid: $id + ); + } + + /** + * Create a new object from array data + * + * @param array $object The object data + * @return ObjectEntity The created object + */ + public function createFromArray(array $object) { + return $this->saveObject( + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } + + /** + * Update an existing object from array data + * + * @param string $id The object ID to update + * @param array $object The new object data + * @param bool $updatedObject Whether this is an update operation + * @return ObjectEntity The updated object + */ + public function updateFromArray(string $id, array $object, bool $updatedObject) { + // Add ID to object data for update + $object['id'] = $id; + + return $this->saveObject( + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } + + /** + * Delete an object + * + * @param array|\JsonSerializable $object The object to delete + * @return bool True if deletion was successful + */ + public function delete(array|\JsonSerializable $object): bool + { + // Convert JsonSerializable objects to array + if($object instanceof \JsonSerializable === true) { + $object = $object->jsonSerialize(); + } + + return $this->deleteObject( + register: $this->registerMapper->find($this->getRegister()), + schema: $this->schemaMapper->find($this->getSchema()), + uuid: $object['id'] + ); + } + + /** + * Find all objects matching given criteria + * + * @param int|null $limit Maximum number of results + * @param int|null $offset Starting offset for pagination + * @param array $filters Filter criteria + * @param array $sort Sorting criteria + * @param string|null $search Search term + * @return array List of matching objects + */ + public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + { + $objects = $this->getObjects( + register: $this->getRegister(), + schema: $this->getSchema(), + limit: $limit, + offset: $offset, + filters: $filters, + sort: $sort, + search: $search + ); + + return $objects; + } + + /** + * Count total objects matching filters + * + * @param array $filters Filter criteria + * @param string|null $search Search term + * @return int Total count + */ + public function count(array $filters = [], ?string $search = null): int + { + // Add register and schema filters if set + if($this->getSchema() !== null && $this->getRegister() !== null) { + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + } + + return $this->objectEntityMapper + ->countAll(filters: $filters, search: $search); + } + + /** + * Find multiple objects by their IDs + * + * @param array $ids Array of object IDs to find + * @return array Array of found objects + */ + public function findMultiple(array $ids): array + { + $result = []; + foreach($ids as $id) { + $result[] = $this->find($id); + } + + return $result; + } + + /** + * Get aggregations for objects matching filters + * + * @param array $filters Filter criteria + * @param string|null $search Search term + * @return array Aggregation results + */ + public function getAggregations(array $filters, ?string $search = null): array + { + $mapper = $this->getMapper(objectType: 'objectEntity'); + + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + + // Only ObjectEntityMapper supports facets + if ($mapper instanceof ObjectEntityMapper === true) { + $facets = $this->objectEntityMapper->getFacets($filters, $search); + return $facets; + } + + return []; + } + + /** + * Extract object data from an entity + * + * @param mixed $object The object to extract data from + * @return mixed The extracted object data + */ + private function getDataFromObject(mixed $object) { + return $object->getObject(); + } + + /** + * Gets all objects of a specific type. + * + * @param string|null $objectType The type of objects to retrieve. + * @param int|null $register + * @param int|null $schema + * @param int|null $limit The maximum number of objects to retrieve. + * @param int|null $offset The offset from which to start retrieving objects. + * @param array $filters + * @return array The retrieved objects. + * @throws \Exception + */ + public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + { + // Set object type and filters if register and schema are provided + if($objectType === null && $register !== null && $schema !== null) { + $objectType = 'objectEntity'; + $filters['register'] = $register; + $filters['schema'] = $schema; + } + + // Get the appropriate mapper for the object type + $mapper = $this->getMapper($objectType); + + // Use the mapper to find and return all objects of the specified type + return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); + } + + /** + * Save an object + * + * @param Register|string $register The register to save the object to. + * @param Schema|string $schema The schema to save the object to. + * @param array $object The data to be saved. + * + * @return ObjectEntity The resulting object. + */ + public function saveObject(int $register, int $schema, array $object): ObjectEntity + { + // Convert register and schema to their respective objects if they are strings + if (is_string($register)) { + $register = $this->registerMapper->find($register); + } + if (is_string($schema)) { + $schema = $this->schemaMapper->find($schema); + } + + // Check if object already exists + if(isset($object['id']) === true) { + $objectEntity = $this->objectEntityMapper->findByUuid( + $this->registerMapper->find($register), + $this->schemaMapper->find($schema), + $object['id'] + ); + } + + // Create new entity if none exists + if($objectEntity === null){ + $objectEntity = new ObjectEntity(); + $objectEntity->setRegister($register); + $objectEntity->setSchema($schema); + } + + // Handle UUID assignment + if (isset($object['id']) && !empty($object['id'])) { + $objectEntity->setUuid($object['id']); + } else { + $objectEntity->setUuid(Uuid::v4()); + $object['id'] = $objectEntity->getUuid(); + } + + // Store old version for audit trail + $oldObject = clone $objectEntity; + $objectEntity->setObject($object); + + // Ensure UUID exists + if (empty($objectEntity->getUuid())) { + $objectEntity->setUuid(Uuid::v4()); + } + + // Update or insert based on whether ID exists + if($objectEntity->getId()){ + $objectEntity = $this->objectEntityMapper->update($objectEntity); + $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); + } + else { + $objectEntity = $this->objectEntityMapper->insert($objectEntity); + $this->auditTrailMapper->createAuditTrail(new: $objectEntity); + } + + return $objectEntity; + } + + /** + * Get an object + * + * @param Register $register The register to get the object from + * @param Schema $schema The schema of the object + * @param string $uuid The UUID of the object to get + * + * @return ObjectEntity The resulting object + * @throws \Exception If source type is unsupported + */ + public function getObject(Register $register, Schema $schema, string $uuid): ObjectEntity + { + // Handle internal source + if ($register->getSource() === 'internal' || $register->getSource() === '') { + return $this->objectEntityMapper->findByUuid($register, $schema, $uuid); + } + + //@todo mongodb support + + throw new \Exception('Unsupported source type'); + } + + /** + * Delete an object + * + * @param Register $register The register to delete from + * @param Schema $schema The schema of the object + * @param string $uuid The UUID of the object to delete + * + * @return bool True if deletion was successful + * @throws \Exception If source type is unsupported + */ + public function deleteObject(Register $register, Schema $schema, string $uuid): bool + { + // Handle internal source + if ($register->getSource() === 'internal' || $register->getSource() === '') { + $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); + $this->objectEntityMapper->delete($object); + return true; + } + + //@todo mongodb support + + throw new \Exception('Unsupported source type'); + } + + /** + * Gets the appropriate mapper based on the object type. + * + * @param string|null $objectType The type of object to retrieve the mapper for + * @param int|null $register Optional register ID + * @param int|null $schema Optional schema ID + * @return mixed The appropriate mapper + * @throws \InvalidArgumentException If unknown object type + */ + public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null) + { + // Return self if register and schema provided + if($register !== null && $schema !== null) { + $this->setSchema($schema); + $this->setRegister($register); + return $this; + } + + // Return appropriate mapper based on object type + switch ($objectType) { + case 'register': + return $this->registerMapper; + case 'schema': + return $this->schemaMapper; + case 'objectEntity': + return $this->objectEntityMapper; + default: + throw new \InvalidArgumentException("Unknown object type: $objectType"); + } + } + + /** + * Gets multiple objects based on the object type and ids. + * + * @param string $objectType The type of objects to retrieve + * @param array $ids The ids of the objects to retrieve + * @return array The retrieved objects + * @throws \InvalidArgumentException If unknown object type + */ + public function getMultipleObjects(string $objectType, array $ids) + { + // Process the ids to handle different formats + $processedIds = array_map(function($id) { + if (is_object($id) && method_exists($id, 'getId')) { + return $id->getId(); + } elseif (is_array($id) && isset($id['id'])) { + return $id['id']; + } else { + return $id; + } + }, $ids); + + // Clean up URIs to get just the ID portion + $cleanedIds = array_map(function($id) { + if (filter_var($id, FILTER_VALIDATE_URL)) { + $parts = explode('/', rtrim($id, '/')); + return end($parts); + } + return $id; + }, $processedIds); + + // Get mapper and find objects + $mapper = $this->getMapper($objectType); + return $mapper->findMultiple($cleanedIds); + } + + /** + * Extends an entity with related objects based on the extend array. + * + * @param mixed $entity The entity to extend + * @param array $extend Properties to extend with related data + * @return array The extended entity as an array + * @throws \Exception If property not found or no mapper available + */ + public function extendEntity(array $entity, array $extend): array + { + // Convert entity to array if needed + if(is_array($entity)) { + $result = $entity; + } else { + $result = $entity->jsonSerialize(); + } + + // Process each property to extend + foreach ($extend as $property) { + $singularProperty = rtrim($property, 's'); + + // Check if property exists + if (array_key_exists(key: $property, array: $result) === true) { + $value = $result[$property]; + if (empty($value)) { + continue; + } + } elseif (array_key_exists(key: $singularProperty, array: $result)) { + $value = $result[$singularProperty]; + } else { + throw new \Exception("Property '$property' or '$singularProperty' is not present in the entity."); + } + + // Try to get mapper for property + $propertyObject = $property; + try { + $mapper = $this->getMapper(objectType: $property); + $propertyObject = $singularProperty; + } catch (\Exception $e) { + try { + $mapper = $this->getMapper(objectType: $singularProperty); + $propertyObject = $singularProperty; + } catch (\Exception $e) { + throw new \Exception("No mapper available for property '$property'."); + } + } + + // Extend with related objects + if (is_array($value) === true) { + $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value); + } else { + $objectId = is_object(value: $value) ? $value->getId() : $value; + $result[$property] = $mapper->find($objectId); + } + } + + return $result; + } + + /** + * Get all registers extended with their schemas + * + * @return array The registers with schema data + * @throws \Exception If extension fails + */ + public function getRegisters(): array + { + // Get all registers + $registers = $this->registerMapper->findAll(); + + // Convert to arrays + $registers = array_map(function($object) { + return $object->jsonSerialize(); + }, $registers); + + // Extend with schemas + $extend = ['schemas']; + if(empty($extend) === false) { + $registers = array_map(function($object) use ($extend) { + return $this->extendEntity(entity: $object, extend: $extend); + }, $registers); + } + + return $registers; + } + + /** + * Get current register ID + * + * @return int The register ID + */ + public function getRegister(): int + { + return $this->register; + } + + /** + * Set current register ID + * + * @param int $register The register ID to set + */ + public function setRegister(int $register): void + { + $this->register = $register; + } + + /** + * Get current schema ID + * + * @return int The schema ID + */ + public function getSchema(): int + { + return $this->schema; + } + + /** + * Set current schema ID + * + * @param int $schema The schema ID to set + */ + public function setSchema(int $schema): void + { + $this->schema = $schema; + } + + /** + * Get the audit trail for a specific object + * + * @todo: register and schema parameters are not needed anymore + * + * @param int $register The register ID + * @param int $schema The schema ID + * @param string $id The object ID + * @return array The audit trail entries + */ + public function getAuditTrail(int $register, int $schema, string $id): array + { + $filters = [ + 'object' => $id + ]; + + return $this->auditTrailMapper->findAllUuid(idOrUuid: $id); + } } diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index a5cb602154..9ef5d6cdc0 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -7,42 +7,62 @@ use OCP\IURLGenerator; use Symfony\Component\Uid\Uuid; +/** + * Service class for handling search functionality + * + * This class provides methods for searching objects across multiple sources, + * merging results, handling facets/aggregations, and processing search parameters + */ class SearchService { + /** @var Client HTTP client for making requests */ public $client; + /** @var array Default base object configuration */ public const BASE_OBJECT = [ 'database' => 'objects', 'collection' => 'json', ]; + /** + * Constructor + * + * @param IURLGenerator $urlGenerator URL generator service + */ public function __construct( private readonly IURLGenerator $urlGenerator, ) { $this->client = new Client(); } + /** + * Merges facet counts from two aggregations + * + * @param array $existingAggregation Original aggregation array + * @param array $newAggregation New aggregation array to merge + * @return array Merged facet counts + */ public function mergeFacets(array $existingAggregation, array $newAggregation): array { $results = []; $existingAggregationMapped = []; $newAggregationMapped = []; + // Map existing aggregation counts by ID foreach ($existingAggregation as $value) { $existingAggregationMapped[$value['_id']] = $value['count']; } - + // Merge new aggregation counts, adding to existing where present foreach ($newAggregation as $value) { if (isset ($existingAggregationMapped[$value['_id']]) === true) { $newAggregationMapped[$value['_id']] = $existingAggregationMapped[$value['_id']] + $value['count']; } else { $newAggregationMapped[$value['_id']] = $value['count']; } - } - + // Format results array with merged counts foreach (array_merge(array_diff($existingAggregationMapped, $newAggregationMapped), array_diff($newAggregationMapped, $existingAggregationMapped)) as $key => $value) { $results[] = ['_id' => $key, 'count' => $value]; } @@ -50,13 +70,20 @@ public function mergeFacets(array $existingAggregation, array $newAggregation): return $results; } + /** + * Merges multiple aggregation arrays + * + * @param array|null $existingAggregations Original aggregations + * @param array|null $newAggregations New aggregations to merge + * @return array Merged aggregations + */ private function mergeAggregations(?array $existingAggregations, ?array $newAggregations): array { if ($newAggregations === null) { return []; } - + // Merge each aggregation key foreach ($newAggregations as $key => $aggregation) { if (isset($existingAggregations[$key]) === false) { $existingAggregations[$key] = $aggregation; @@ -67,18 +94,30 @@ private function mergeAggregations(?array $existingAggregations, ?array $newAggr return $existingAggregations; } + /** + * Comparison function for sorting results by score + * + * @param array $a First result array + * @param array $b Second result array + * @return int Comparison result (-1, 0, 1) + */ public function sortResultArray(array $a, array $b): int { return $a['_score'] <=> $b['_score']; } - - /** - * - */ + /** + * Main search function that queries multiple sources and merges results + * + * @param array $parameters Search parameters and filters + * @param array $elasticConfig Elasticsearch configuration + * @param array $dbConfig Database configuration + * @param array $catalogi List of catalogs to search + * @return array Combined search results with facets and pagination info + */ public function search(array $parameters, array $elasticConfig, array $dbConfig, array $catalogi = []): array { - + // Initialize results arrays $localResults['results'] = []; $localResults['facets'] = []; @@ -86,14 +125,14 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $limit = isset($parameters['.limit']) === true ? $parameters['.limit'] : 30; $page = isset($parameters['.page']) === true ? $parameters['.page'] : 1; + // Query elastic if configured if ($elasticConfig['location'] !== '') { $localResults = $this->elasticService->searchObject(filters: $parameters, config: $elasticConfig, totalResults: $totalResults,); } $directory = $this->directoryService->listDirectory(limit: 1000); -// $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); - + // Return early if no directory entries if (count($directory) === 0) { $pages = (int) ceil($totalResults / $limit); return [ @@ -112,7 +151,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $searchEndpoints = []; - + // Build search requests for each endpoint $promises = []; foreach ($directory as $instance) { if ( @@ -128,15 +167,16 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, unset($parameters['.catalogi']); + // Create async requests for each endpoint foreach ($searchEndpoints as $searchEndpoint => $catalogi) { $parameters['_catalogi'] = $catalogi; - - $promises[] = $this->client->getAsync($searchEndpoint, ['query' => $parameters]); } + // Wait for all requests to complete $responses = Utils::settle($promises)->wait(); + // Process responses and merge results foreach ($responses as $response) { if ($response['state'] === 'fulfilled') { $responseData = json_decode( @@ -157,6 +197,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $pages = (int) ceil($totalResults / $limit); + // Return combined results with pagination info return [ 'results' => $results, 'facets' => $aggregations, @@ -379,7 +420,6 @@ public function parseQueryString (string $queryString = ''): array ); } - return $vars; }