diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index 9cc6802..e1aa1f2 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -33,7 +33,7 @@ public function __construct( /** * Returns the template of the main app's page - * + * * This method renders the main page of the application, adding any necessary data to the template. * * @NoAdminRequired @@ -42,17 +42,17 @@ public function __construct( * @return TemplateResponse The rendered template response */ public function page(): TemplateResponse - { + { return new TemplateResponse( 'openconnector', 'index', [] ); } - + /** * Retrieves a list of all objects - * + * * This method returns a JSON response containing an array of all objects in the system. * * @NoAdminRequired @@ -69,12 +69,12 @@ public function index(ObjectService $objectService, SearchService $searchService $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->objectEntityMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + return new JSONResponse(['results' => $this->objectEntityMapper->findAll(filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); } /** * Retrieves a single object by its ID - * + * * This method returns a JSON response containing the details of a specific object. * * @NoAdminRequired @@ -94,7 +94,7 @@ public function show(string $id): JSONResponse /** * Creates a new object - * + * * This method creates a new object based on POST data. * * @NoAdminRequired @@ -111,17 +111,17 @@ public function create(): JSONResponse unset($data[$key]); } } - + if (isset($data['id'])) { unset($data['id']); } - + return new JSONResponse($this->objectEntityMapper->createFromArray(object: $data)); } /** * Updates an existing object - * + * * This method updates an existing object based on its ID. * * @NoAdminRequired @@ -147,7 +147,7 @@ public function update(int $id): JSONResponse /** * Deletes an object - * + * * This method deletes an object based on its ID. * * @NoAdminRequired @@ -162,4 +162,4 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } -} \ No newline at end of file +} diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php index 44bc3ea..66099ae 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -60,14 +60,16 @@ public function hydrate(array $object): self public function jsonSerialize(): array { - return [ - 'id' => $this->id, - 'uuid' => $this->uuid, - 'register' => $this->register, - 'schema' => $this->schema, - 'object' => $this->object, - 'updated' => isset($this->updated) ? $this->updated->format('c') : null, - 'created' => isset($this->created) ? $this->created->format('c') : null - ]; +// return [ +// 'id' => $this->id, +// 'uuid' => $this->uuid, +// 'register' => $this->register, +// 'schema' => $this->schema, +// 'object' => $this->object, +// 'updated' => isset($this->updated) ? $this->updated->format('c') : null, +// 'created' => isset($this->created) ? $this->created->format('c') : null +// ]; + + return $this->object; } } diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 4641e00..88e830b 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -2,9 +2,12 @@ namespace OCA\OpenRegister\Db; +use Doctrine\DBAL\Platforms\MySQLPlatform; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Service\IDatabaseJsonService; +use OCA\OpenRegister\Service\MySQLJsonService; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -13,9 +16,17 @@ class ObjectEntityMapper extends QBMapper { - public function __construct(IDBConnection $db) + private IDatabaseJsonService $databaseJsonService; + + public const MAIN_FILTERS = ['register', 'schema', 'uuid', 'created', 'updated']; + + public function __construct(IDBConnection $db, MySQLJsonService $mySQLJsonService) { parent::__construct($db, 'openregister_objects'); + + if($db->getDatabasePlatform() instanceof MySQLPlatform === true) { + $this->databaseJsonService = $mySQLJsonService; + } } /** @@ -89,6 +100,37 @@ public function findByRegisterAndSchema(string $register, string $schema): Objec return $this->findEntities(query: $qb); } + public function countAll(?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): int + { + $qb = $this->db->getQueryBuilder(); + + $qb->selectAlias(select: $qb->createFunction(call: 'count(id)'), alias: 'count') + ->from(from: 'openregister_objects'); + foreach ($filters as $filter => $value) { + if ($value === 'IS NOT NULL' && in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL' && in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { + $qb->andWhere($qb->expr()->isNull($filter)); + } else if (in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } + } + + if (!empty($searchConditions)) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + $qb = $this->databaseJsonService->filterJson($qb, $filters); + + $result = $qb->executeQuery(); + + $count = $result->fetchAll()[0]['count']; + + return $count; + } + /** * Find all ObjectEntitys * @@ -109,11 +151,11 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach ($filters as $filter => $value) { - if ($value === 'IS NOT NULL') { + if ($value === 'IS NOT NULL' && in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { $qb->andWhere($qb->expr()->isNotNull($filter)); - } elseif ($value === 'IS NULL') { + } elseif ($value === 'IS NULL' && in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { $qb->andWhere($qb->expr()->isNull($filter)); - } else { + } else if (in_array(needle: $filter, haystack: self::MAIN_FILTERS) === true) { $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); } } @@ -124,6 +166,8 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters $qb->setParameter($param, $value); } } + $qb = $this->databaseJsonService->filterJson($qb, $filters); + return $this->findEntities(query: $qb); } @@ -148,4 +192,36 @@ public function updateFromArray(int $id, array $object): ObjectEntity return $this->update($obj); } + + public function getFacets(array $filters = []) + { + if(key_exists(key: 'register', array: $filters) === true) { + $register = $filters['register']; + } + if(key_exists(key: 'schema', array: $filters) === true) { + $schema = $filters['schema']; + } + + $fields = []; + if(isset($filters['_queries'])) { + $fields = $filters['_queries']; + } + + unset( + $filters['_fields'], + $filters['register'], + $filters['schema'], + $filters['created'], + $filters['updated'], + $filters['uuid'] + ); + + return $this->databaseJsonService->getAggregations( + builder: $this->db->getQueryBuilder(), + fields: $fields, + register: $register, + schema: $schema, + filters: $filters + ); + } } diff --git a/lib/Service/IDatabaseJsonService.php b/lib/Service/IDatabaseJsonService.php new file mode 100644 index 0000000..7424d18 --- /dev/null +++ b/lib/Service/IDatabaseJsonService.php @@ -0,0 +1,11 @@ +$value) { + $builder->createNamedParameter(value: $value, placeHolder: ":value$filter"); + $builder->createNamedParameter(value: "$.$filter", placeHolder: ":path$filter"); + + $builder + ->andWhere("json_extract(object, :path$filter) = :value$filter"); + } + + return $builder; + } + + public function getAggregations(IQueryBuilder $builder, array $fields, int $register, int $schema, array $filters = []): array + { + $facets = []; + + foreach($fields as $field) { + $builder->createNamedParameter(value: "$.$field", placeHolder: ":$field"); + + + $builder + ->selectAlias($builder->createFunction("json_unquote(json_extract(object, :$field))"), '_id') + ->selectAlias($builder->createFunction("count(*)"), 'count') + ->from('openregister_objects') + ->where( + $builder->expr()->eq('register', $builder->createNamedParameter($register, IQueryBuilder::PARAM_INT)), + $builder->expr()->eq('schema', $builder->createNamedParameter($schema, IQueryBuilder::PARAM_INT)), + ) + ->groupBy('_id'); + + $builder = $this->filterJson($builder, $filters); + + $result = $builder->executeQuery(); + $facets[$field] = $result->fetchAll(); + + $builder->resetQueryParts(); + $builder->setParameters([]); + + } + return $facets; + } +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 2911d17..af6d179 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -14,6 +14,10 @@ class ObjectService { + + private int $register; + private int $schema; + private $callLogMapper; /** @@ -28,6 +32,127 @@ public function __construct(ObjectEntityMapper $objectEntityMapper, RegisterMapp $this->schemaMapper = $schemaMapper; } + 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 + { + $objects = $this->getObjects( + register: $this->getRegister(), + schema: $this->getSchema(), + limit: $limit, + offset: $offset, + filters: $filters + ); +// $data = array_map([$this, 'getDataFromObject'], $objects); + + return $objects; + } + + public function count(array $filters = []): int + { + if($this->getSchema() !== null && $this->getRegister() !== null) { + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + } + $count = $this->objectEntityMapper + ->countAll(filters: $filters); + + return $count; + } + + public function findMultiple(array $ids): array + { + $result = []; + foreach($ids as $id) { + $result[] = $this->find($id); + } + + return $result; + } + + public function getAggregations(array $filters): array + { + $mapper = $this->getMapper(objectType: 'objectEntity'); + + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + + if ($mapper instanceof ObjectEntityMapper === true) { + $facets = $this->objectEntityMapper->getFacets($filters); + 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 + { + 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); + } + /** * Save an object * @@ -37,7 +162,7 @@ public function __construct(ObjectEntityMapper $objectEntityMapper, RegisterMapp * * @return ObjectEntity The resulting object. */ - public function saveObject($register, $schema, array $object): ObjectEntity + public function saveObject(int $register, int $schema, array $object): ObjectEntity { // Convert register and schema to their respective objects if they are strings @@ -48,13 +173,15 @@ public function saveObject($register, $schema, array $object): ObjectEntity $schema = $this->schemaMapper->find($schema); } - // Does the object already exist? - $objectEntity = $this->objectEntityMapper->findByUuid($register, $schema, $object['id']); + if(isset($object['id']) === true) { + // Does the object already exist? + $objectEntity = $this->objectEntityMapper->findByUuid($this->registerMapper->find($register), $this->schemaMapper->find($schema), $object['id']); + } if($objectEntity === null){ $objectEntity = new ObjectEntity(); - $objectEntity->setRegister($register->getId()); - $objectEntity->setSchema($schema->getId()); + $objectEntity->setRegister($register); + $objectEntity->setSchema($schema); ///return $this->objectEntityMapper->update($objectEntity); } @@ -91,11 +218,12 @@ public function saveObject($register, $schema, array $object): ObjectEntity * * @return ObjectEntity The resulting object. */ - public function getObject(Register $register, string $uuid): ObjectEntity + 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') { - return $this->objectEntityMapper->findByUuid($register,$uuid); + if ($register->getSource() === 'internal' || $register->getSource() === '') { + return $this->objectEntityMapper->findByUuid($register, $schema, $uuid); } //@todo mongodb support @@ -112,12 +240,14 @@ public function getObject(Register $register, string $uuid): ObjectEntity * @return ObjectEntity The resulting object. */ - public function deleteObject(Register $register, string $uuid) + 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') { - $object = $this->objectEntityMapper->findByUuid($uuid); + 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 @@ -134,8 +264,15 @@ public function deleteObject(Register $register, string $uuid) * @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) + 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': @@ -149,6 +286,8 @@ public function getMapper(string $objectType) } } + + /** * Gets multiple objects based on the object type and ids. * @@ -277,4 +416,24 @@ public function getRegisters(): array 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; + } } diff --git a/lib/Service/PostgresqlScripts/fetch_aggregations_example.sql b/lib/Service/PostgresqlScripts/fetch_aggregations_example.sql new file mode 100644 index 0000000..142c373 --- /dev/null +++ b/lib/Service/PostgresqlScripts/fetch_aggregations_example.sql @@ -0,0 +1 @@ +select (object#>>'{tooi}') as gemeentecode, count(*) as count from oc_openregister_objects where register = '2' group by gemeentecode; \ No newline at end of file diff --git a/lib/Service/PostgresqlScripts/filter_json.sql b/lib/Service/PostgresqlScripts/filter_json.sql new file mode 100644 index 0000000..ff20bff --- /dev/null +++ b/lib/Service/PostgresqlScripts/filter_json.sql @@ -0,0 +1,3 @@ +SELECT * FROM public.oc_openregister_objects +WHERE register = '2' AND object#>>'{tooi}' = '0268' OR object#>>'{tooi}' = '0935' +ORDER BY id ASC \ No newline at end of file diff --git a/src/entities/source/source.mock.ts b/src/entities/source/source.mock.ts index a9e1f95..c6ba94c 100644 --- a/src/entities/source/source.mock.ts +++ b/src/entities/source/source.mock.ts @@ -4,10 +4,10 @@ import { TSource } from './source.types' export const mockSourceData = (): TSource[] => [ { id: 1, - title: 'Main PostgreSQL Database', + title: 'Main MongoDB Database', description: 'Primary database for user data', - databaseUrl: 'postgresql://user:password@localhost:5432/maindb', - type: 'postgresql', + databaseUrl: 'mongodb://user:password@localhost:27017/maindb', + type: 'mongodb', updated: new Date().toISOString(), created: new Date().toISOString(), }, diff --git a/src/entities/source/source.ts b/src/entities/source/source.ts index ffa715e..b3ac52c 100644 --- a/src/entities/source/source.ts +++ b/src/entities/source/source.ts @@ -7,7 +7,7 @@ export class Source implements TSource { public title: string public description: string public databaseUrl: string - public type: string + public type: 'internal' | 'mongodb' public updated: string public created: string @@ -16,7 +16,7 @@ export class Source implements TSource { this.title = source.title || '' this.description = source.description || '' this.databaseUrl = source.databaseUrl || '' - this.type = source.type || '' + this.type = source.type || 'internal' this.updated = source.updated || '' this.created = source.created || '' } @@ -27,7 +27,7 @@ export class Source implements TSource { title: z.string().min(1), description: z.string(), databaseUrl: z.string().url(), - type: z.string(), + type: z.enum(['internal', 'mongodb']), }) return schema.safeParse(this) diff --git a/src/entities/source/source.types.ts b/src/entities/source/source.types.ts index 05e55d1..b3f1e0b 100644 --- a/src/entities/source/source.types.ts +++ b/src/entities/source/source.types.ts @@ -3,7 +3,7 @@ export type TSource = { title: string; description: string; databaseUrl: string; - type: string; + type: 'internal' | 'mongodb'; updated: string; created: string; } diff --git a/src/modals/source/EditSource.vue b/src/modals/source/EditSource.vue index 6b6cac9..468fcb1 100644 --- a/src/modals/source/EditSource.vue +++ b/src/modals/source/EditSource.vue @@ -24,9 +24,10 @@ import { sourceStore, navigationStore } from '../../store/store.js' - +