diff --git a/.eslintrc.js b/.eslintrc.js index 9175db9..2a33e82 100755 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,5 +5,6 @@ module.exports = { rules: { 'jsdoc/require-jsdoc': 'off', 'vue/first-attribute-linebreak': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fe76d72 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "files.autoSave": "afterDelay", + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true, + "eslint.format.enable": true, + "cSpell.words": [ + "depubliceren", + "Depubliceren", + "gedepubliceerd", + "Matadata", + "nextcloud", + "opencatalogi", + "organisation", + "Organisation", + "organisations", + "Organisations", + "pinia", + "Toegangs" + ], + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, +} diff --git a/appinfo/routes.php b/appinfo/routes.php index 6cba70c..93948ea 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -5,8 +5,10 @@ 'Registers' => ['url' => 'api/registers'], 'Schemas' => ['url' => 'api/schemas'], 'Sources' => ['url' => 'api/sources'], + 'Objects' => ['url' => 'api/objects'], ], 'routes' => [ - ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], + ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], + ['name' => 'registers#objects', 'url' => '/api/registers-objects/{register}/{schema}', 'verb' => 'GET'], ], ]; diff --git a/css/main.css b/css/main.css index 19ef91a..290da6e 100755 --- a/css/main.css +++ b/css/main.css @@ -1,7 +1,7 @@ :root { - --OC-margin-10: 10px; - --OC-margin-20: 20px; - --OC-margin-50: 50px; + --OR-margin-10: 10px; + --OR-margin-20: 20px; + --OR-margin-50: 50px; } /* Pages */ @@ -18,11 +18,15 @@ /* Lists */ .listHeader { + display: flex; + position: sticky; top: 0; z-index: 1000; background-color: var(--color-main-background); border-bottom: 1px solid var(--color-border); + flex-direction: row; + align-items: center; } .searchField { @@ -39,9 +43,9 @@ /* Detail pages */ .detailContainer { - margin-block-start: var(--OC-margin-20); - margin-inline-start: var(--OC-margin-20); - margin-inline-end: var(--OC-margin-20); + margin-block-start: var(--OR-margin-20); + margin-inline-start: var(--OR-margin-20); + margin-inline-end: var(--OR-margin-20); } .tabContainer > * ul > li { @@ -79,7 +83,7 @@ } .tabContainer > * div[role="tabpanel"] { - margin-block-start: var(--OC-margin-10); + margin-block-start: var(--OR-margin-10); } .tabPanel { @@ -107,6 +111,11 @@ } /* Modals */ +.modalContent { + margin: var(--OR-margin-50); + text-align: center; +} + .form-group > * { margin-block-end: 10px; } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..375daca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: "3.5" +volumes: + nextcloud: + apps: + db: + config: + +services: + db: + image: mariadb:10.6 + restart: always + command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW + volumes: + - db:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD='!ChangeMe!' + - MYSQL_PASSWORD='!ChangeMe!' + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud + + init-ubuntu: + image: ubuntu + command: sh /home/ubuntu/docker/init-ubuntu.sh + volumes: + - ./docker:/home/ubuntu/docker + - .:/home/ubuntu/app + + nextcloud: + user: root + container_name: nextcloud +# entrypoint: occ app:enable openregister + image: nextcloud + restart: always + ports: + - 8080:80 + links: + - db + volumes: + - nextcloud:/var/www/html:rw + - ./custom_apps:/var/www/html/custom_apps + - .:/var/www/html/custom_apps/openregister + environment: + - MYSQL_PASSWORD='!ChangeMe!' + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud + - MYSQL_HOST=db + - TZ=Europe/Amsterdam + - NEXTCLOUD_ADMIN_USER=admin + - NEXTCLOUD_ADMIN_PASSWORD=admin diff --git a/docker/init-ubuntu.sh b/docker/init-ubuntu.sh new file mode 100644 index 0000000..12b53f0 --- /dev/null +++ b/docker/init-ubuntu.sh @@ -0,0 +1,8 @@ +apt update; +apt install npm composer php-gd php-zip -y + +cd /home/ubuntu/app +npm i +npm run dev + +composer install \ No newline at end of file diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php new file mode 100644 index 0000000..9cc6802 --- /dev/null +++ b/lib/Controller/ObjectsController.php @@ -0,0 +1,165 @@ +request->getParams(); + $fieldsToSearch = ['uuid', 'register', 'schema']; + + $searchParams = $searchService->createMySQLSearchParams(filters: $filters); + $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)]); + } + + /** + * Retrieves a single object by its ID + * + * This method returns a JSON response containing the details of a specific object. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the object to retrieve + * @return JSONResponse A JSON response containing the object details + */ + public function show(string $id): JSONResponse + { + try { + return new JSONResponse($this->objectEntityMapper->find(id: (int) $id)); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + } + + /** + * Creates a new object + * + * This method creates a new object based on POST data. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse A JSON response containing the created object + */ + public function create(): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + 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 + * @NoCSRFRequired + * + * @param string $id The ID of the object to update + * @return JSONResponse A JSON response containing the updated object details + */ + public function update(int $id): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + unset($data[$key]); + } + } + if (isset($data['id'])) { + unset($data['id']); + } + return new JSONResponse($this->objectEntityMapper->updateFromArray(id: (int) $id, object: $data)); + } + + /** + * Deletes an object + * + * This method deletes an object based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the object to delete + * @return JSONResponse An empty JSON response + */ + public function destroy(int $id): JSONResponse + { + $this->objectEntityMapper->delete($this->objectEntityMapper->find((int) $id)); + + return new JSONResponse([]); + } +} \ No newline at end of file diff --git a/lib/Controller/RegistersController.php b/lib/Controller/RegistersController.php index 58270c1..424fe67 100644 --- a/lib/Controller/RegistersController.php +++ b/lib/Controller/RegistersController.php @@ -4,8 +4,9 @@ use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\SearchService; -use OCA\OpenRegister\Db\Source; -use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\ObjectEntityMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -15,7 +16,7 @@ class RegistersController extends Controller { /** - * Constructor for the SourcesController + * Constructor for the RegistersController * * @param string $appName The name of the app * @param IRequest $request The request object @@ -25,7 +26,8 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly SourceMapper $sourceMapper + private readonly RegisterMapper $registerMapper, + private readonly ObjectEntityMapper $objectEntityMapper ) { parent::__construct($appName, $request); @@ -51,56 +53,56 @@ public function page(): TemplateResponse } /** - * Retrieves a list of all sources + * Retrieves a list of all registers * - * This method returns a JSON response containing an array of all sources in the system. + * This method returns a JSON response containing an array of all registers in the system. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the list of sources + * @return JSONResponse A JSON response containing the list of registers */ public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->sourceMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + return new JSONResponse(['results' => $this->registerMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); } /** - * Retrieves a single source by its ID + * Retrieves a single register by its ID * - * This method returns a JSON response containing the details of a specific source. + * This method returns a JSON response containing the details of a specific register. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to retrieve - * @return JSONResponse A JSON response containing the source details + * @param string $id The ID of the register to retrieve + * @return JSONResponse A JSON response containing the register details */ public function show(string $id): JSONResponse { try { - return new JSONResponse($this->sourceMapper->find(id: (int) $id)); + return new JSONResponse($this->registerMapper->find(id: (int) $id)); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } } /** - * Creates a new source + * Creates a new register * - * This method creates a new source based on POST data. + * This method creates a new register based on POST data. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the created source + * @return JSONResponse A JSON response containing the created register */ public function create(): JSONResponse { @@ -116,19 +118,19 @@ public function create(): JSONResponse unset($data['id']); } - return new JSONResponse($this->sourceMapper->createFromArray(object: $data)); + return new JSONResponse($this->registerMapper->createFromArray(object: $data)); } /** - * Updates an existing source + * Updates an existing register * - * This method updates an existing source based on its ID. + * This method updates an existing register based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to update - * @return JSONResponse A JSON response containing the updated source details + * @param string $id The ID of the register to update + * @return JSONResponse A JSON response containing the updated register details */ public function update(int $id): JSONResponse { @@ -142,24 +144,42 @@ public function update(int $id): JSONResponse if (isset($data['id'])) { unset($data['id']); } - return new JSONResponse($this->sourceMapper->updateFromArray(id: (int) $id, object: $data)); + return new JSONResponse($this->registerMapper->updateFromArray(id: (int) $id, object: $data)); } /** - * Deletes a source + * Deletes a register * - * This method deletes a source based on its ID. + * This method deletes a register based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to delete + * @param string $id The ID of the register to delete * @return JSONResponse An empty JSON response */ public function destroy(int $id): JSONResponse { - $this->sourceMapper->delete($this->sourceMapper->find((int) $id)); + $this->registerMapper->delete($this->registerMapper->find((int) $id)); return new JSONResponse([]); } + + /** + * Get objects + * + * Get all the objects for a register and schema + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $register The ID of the register + * @param string $schema The ID of the schema + * + * @return JSONResponse An empty JSON response + */ + public function objects(int $register, int $schema): JSONResponse + { + return new JSONResponse($this->objectEntityMapper->findByRegisterAndSchema(register: $register, schema: $schema)); + } } \ No newline at end of file diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php index 127e717..82f4223 100644 --- a/lib/Controller/SchemasController.php +++ b/lib/Controller/SchemasController.php @@ -4,8 +4,8 @@ use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\SearchService; -use OCA\OpenRegister\Db\Source; -use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -15,7 +15,7 @@ class SchemasController extends Controller { /** - * Constructor for the SourcesController + * Constructor for the SchemasController * * @param string $appName The name of the app * @param IRequest $request The request object @@ -25,7 +25,7 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly SourceMapper $sourceMapper + private readonly SchemaMapper $schemaMapper ) { parent::__construct($appName, $request); @@ -51,56 +51,56 @@ public function page(): TemplateResponse } /** - * Retrieves a list of all sources + * Retrieves a list of all schemas * - * This method returns a JSON response containing an array of all sources in the system. + * This method returns a JSON response containing an array of all schemas in the system. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the list of sources + * @return JSONResponse A JSON response containing the list of schemas */ public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->sourceMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + return new JSONResponse(['results' => $this->schemaMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); } /** - * Retrieves a single source by its ID + * Retrieves a single schema by its ID * - * This method returns a JSON response containing the details of a specific source. + * This method returns a JSON response containing the details of a specific schema. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to retrieve - * @return JSONResponse A JSON response containing the source details + * @param string $id The ID of the schema to retrieve + * @return JSONResponse A JSON response containing the schema details */ public function show(string $id): JSONResponse { try { - return new JSONResponse($this->sourceMapper->find(id: (int) $id)); + return new JSONResponse($this->schemaMapper->find(id: (int) $id)); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } } /** - * Creates a new source + * Creates a new schema * - * This method creates a new source based on POST data. + * This method creates a new schema based on POST data. * * @NoAdminRequired * @NoCSRFRequired * - * @return JSONResponse A JSON response containing the created source + * @return JSONResponse A JSON response containing the created schema */ public function create(): JSONResponse { @@ -116,19 +116,19 @@ public function create(): JSONResponse unset($data['id']); } - return new JSONResponse($this->sourceMapper->createFromArray(object: $data)); + return new JSONResponse($this->schemaMapper->createFromArray(object: $data)); } /** - * Updates an existing source + * Updates an existing schema * - * This method updates an existing source based on its ID. + * This method updates an existing schema based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to update - * @return JSONResponse A JSON response containing the updated source details + * @param string $id The ID of the schema to update + * @return JSONResponse A JSON response containing the updated schema details */ public function update(int $id): JSONResponse { @@ -142,23 +142,23 @@ public function update(int $id): JSONResponse if (isset($data['id'])) { unset($data['id']); } - return new JSONResponse($this->sourceMapper->updateFromArray(id: (int) $id, object: $data)); + return new JSONResponse($this->schemaMapper->updateFromArray(id: (int) $id, object: $data)); } /** - * Deletes a source + * Deletes a schema * - * This method deletes a source based on its ID. + * This method deletes a schema based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the source to delete + * @param string $id The ID of the schema to delete * @return JSONResponse An empty JSON response */ public function destroy(int $id): JSONResponse { - $this->sourceMapper->delete($this->sourceMapper->find((int) $id)); + $this->schemaMapper->delete($this->schemaMapper->find((int) $id)); return new JSONResponse([]); } diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 4b10ae7..bc98c2d 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -63,7 +63,7 @@ public function page(): TemplateResponse public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description']; + $fieldsToSearch = ['title', 'description']; $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php new file mode 100644 index 0000000..44bc3ea --- /dev/null +++ b/lib/Db/ObjectEntity.php @@ -0,0 +1,73 @@ +addType(fieldName:'uuid', type: 'string'); + $this->addType(fieldName:'register', type: 'string'); + $this->addType(fieldName:'schema', type: 'string'); + $this->addType(fieldName:'object', type: 'json'); + $this->addType(fieldName:'updated', type: 'datetime'); + $this->addType(fieldName:'created', type: 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + if (isset($object['metadata']) === false) { + $object['metadata'] = []; + } + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + } + } + + return $this; + } + + + 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 + ]; + } +} diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php new file mode 100644 index 0000000..4641e00 --- /dev/null +++ b/lib/Db/ObjectEntityMapper.php @@ -0,0 +1,151 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity(query: $qb); + } + + /** + * Find an object by UUID + * + * @param string $uuid The UUID of the object to find + * @return ObjectEntity The object + */ + public function findByUuid(Register $register, Schema $schema, string $uuid): ObjectEntity|null + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('uuid', $qb->createNamedParameter($uuid)) + ) + ->andWhere( + $qb->expr()->eq('register', $qb->createNamedParameter($register->getId())) + ) + ->andWhere( + $qb->expr()->eq('schema', $qb->createNamedParameter($schema->getId())) + ); + + try { + return $this->findEntity($qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + } + + /** + * Find objects by register and schema + * + * @param string $register The register to find objects for + * @param string $schema The schema to find objects for + * @return array An array of ObjectEntitys + */ + public function findByRegisterAndSchema(string $register, string $schema): ObjectEntity + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('register', $qb->createNamedParameter($register)) + ) + ->andWhere( + $qb->expr()->eq('schema', $qb->createNamedParameter($schema)) + ); + + return $this->findEntities(query: $qb); + } + + /** + * Find all ObjectEntitys + * + * @param int $limit The number of objects to return + * @param int $offset The offset of the objects to return + * @param array $filters The filters to apply to the objects + * @param array $searchConditions The search conditions to apply to the objects + * @param array $searchParams The search parameters to apply to the objects + * @return array An array of ObjectEntitys + */ + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->setMaxResults($limit) + ->setFirstResult($offset); + + foreach ($filters as $filter => $value) { + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $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); + } + } + + return $this->findEntities(query: $qb); + } + + public function createFromArray(array $object): ObjectEntity + { + $obj = new ObjectEntity(); + $obj->hydrate(object: $object); + if ($obj->getUuid() === null){ + $obj->setUuid(Uuid::v4()); + } + return $this->insert(entity: $obj); + } + + public function updateFromArray(int $id, array $object): ObjectEntity + { + $obj = $this->find($id); + $obj->hydrate($object); + if ($obj->getUuid() === null){ + $obj->setUuid(Uuid::v4()); + } + + return $this->update($obj); + } +} diff --git a/lib/Db/Register.php b/lib/Db/Register.php index 7e4a99d..2bbd0cb 100644 --- a/lib/Db/Register.php +++ b/lib/Db/Register.php @@ -8,41 +8,58 @@ class Register extends Entity implements JsonSerializable { - protected ?string $id = null; protected ?string $title = null; protected ?string $description = null; - protected ?array $schemas = null; + protected ?array $schemas = []; protected ?string $source = null; protected ?string $tablePrefix = null; protected ?DateTime $updated = null; protected ?DateTime $created = null; public function __construct() { - $this->addType('id', 'string'); - $this->addType('title', 'title'); - $this->addType('description', 'string'); - $this->addType('schemas', 'array'); - $this->addType('source', 'string'); - $this->addType('tablePrefix', 'string'); - $this->addType('updated', 'datetime'); - $this->addType('created', 'datetime'); + $this->addType(fieldName: 'title', type: 'string'); + $this->addType(fieldName: 'description', type: 'string'); + $this->addType(fieldName: 'schemas', type: 'json'); + $this->addType(fieldName: 'source', type: 'string'); + $this->addType(fieldName: 'tablePrefix', type: 'string'); + $this->addType(fieldName:'updated', type: 'datetime'); + $this->addType(fieldName:'created', type: 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); } public function hydrate(array $object): self { - foreach($object as $key => $value) { + $jsonFields = $this->getJsonFields(); + + if (isset($object['metadata']) === false) { + $object['metadata'] = []; + } + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = null; + } + $method = 'set'.ucfirst($key); try { $this->$method($value); } catch (\Exception $exception) { - // Error handling can be added here } } return $this; } + public function jsonSerialize(): array { return [ @@ -52,8 +69,8 @@ public function jsonSerialize(): array 'schemas' => $this->schemas, 'source' => $this->source, 'tablePrefix' => $this->tablePrefix, - 'updated' => $this->updated, - 'created' => $this->created + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null ]; } -} \ No newline at end of file +} diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index 2e16cb2..3ec4b57 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -37,7 +37,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setMaxResults($limit) ->setFirstResult($offset); - foreach($filters as $filter => $value) { + foreach ($filters as $filter => $value) { if ($value === 'IS NOT NULL') { $qb->andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index fae5121..8ac7262 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -8,7 +8,7 @@ class Schema extends Entity implements JsonSerializable { - protected ?string $title = null; + protected ?string $title = null; protected ?string $version = null; protected ?string $description = null; protected ?string $summary = null; @@ -20,14 +20,12 @@ class Schema extends Entity implements JsonSerializable protected ?DateTime $created = null; public function __construct() { - $this->addType(fieldName: 'archive', type: 'json'); $this->addType(fieldName: 'title', type: 'string'); $this->addType(fieldName: 'version', type: 'string'); $this->addType(fieldName: 'description', type: 'string'); $this->addType(fieldName: 'summary', type: 'string'); $this->addType(fieldName: 'required', type: 'json'); $this->addType(fieldName: 'properties', type: 'json'); - $this->addType(fieldName: 'source', type: 'string'); $this->addType(fieldName:'updated', type: 'datetime'); $this->addType(fieldName:'created', type: 'datetime'); } @@ -45,7 +43,11 @@ public function hydrate(array $object): self { $jsonFields = $this->getJsonFields(); - foreach($object as $key => $value) { + if (isset($object['metadata']) === false) { + $object['metadata'] = []; + } + + foreach ($object as $key => $value) { if (in_array($key, $jsonFields) === true && $value === []) { $value = null; } @@ -61,6 +63,7 @@ public function hydrate(array $object): self return $this; } + public function jsonSerialize(): array { $properties = []; @@ -72,7 +75,7 @@ public function jsonSerialize(): array } switch ($property['format']) { case 'string': - // For now array as string + // For now array as string case 'array': $properties[$key]['default'] = (string) $property; break; @@ -96,10 +99,8 @@ public function jsonSerialize(): array 'summary' => $this->summary, 'required' => $this->required, 'properties' => $properties, - 'archive' => $this->archive, - 'source' => $this->source, - 'updated' => $this->updated, - 'created' => $this->created + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null, ]; $jsonFields = $this->getJsonFields(); diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 5750eea..cc43d97 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -27,6 +27,15 @@ public function find(int $id): Schema return $this->findEntity(query: $qb); } + public function findMultiple(array $ids): array + { + $result = []; + foreach($ids as $id) { + $result[] = $this->find($id); + } + + return $result; + } public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { @@ -37,7 +46,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setMaxResults($limit) ->setFirstResult($offset); - foreach($filters as $filter => $value) { + foreach ($filters as $filter => $value) { if ($value === 'IS NOT NULL') { $qb->andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 2bd9693..09e92ef 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -8,8 +8,7 @@ class Source extends Entity implements JsonSerializable { - protected ?string $id = null; - protected ?string $name = null; + protected ?string $title = null; protected ?string $description = null; protected ?string $databaseUrl = null; protected ?string $type = null; @@ -17,30 +16,46 @@ class Source extends Entity implements JsonSerializable protected ?DateTime $created = null; public function __construct() { - $this->addType('id', 'string'); - $this->addType('title', 'string'); - $this->addType('description', 'string'); - $this->addType('databaseUrl', 'string'); - $this->addType('type', 'string'); - $this->addType('updated', 'datetime'); - $this->addType('created', 'datetime'); + $this->addType(fieldName: 'title', type: 'string'); + $this->addType(fieldName: 'description', type: 'string'); + $this->addType(fieldName: 'databaseUrl', type: 'string'); + $this->addType(fieldName: 'type', type: 'string'); + $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); } public function hydrate(array $object): self { - foreach($object as $key => $value) { + $jsonFields = $this->getJsonFields(); + + if (isset($object['metadata']) === false) { + $object['metadata'] = []; + } + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = null; + } + $method = 'set'.ucfirst($key); try { $this->$method($value); } catch (\Exception $exception) { - // Error handling can be added here } } return $this; } - public function jsonSerialize(): array { return [ @@ -49,8 +64,8 @@ public function jsonSerialize(): array 'description' => $this->description, 'databaseUrl' => $this->databaseUrl, 'type' => $this->type, - 'updated' => $this->updated, - 'created' => $this->created + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null ]; } -} \ No newline at end of file +} diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index 65034c7..67ded28 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -37,7 +37,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setMaxResults($limit) ->setFirstResult($offset); - foreach($filters as $filter => $value) { + foreach ($filters as $filter => $value) { if ($value === 'IS NOT NULL') { $qb->andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index 04c20d4..0347615 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -40,13 +40,13 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if (!$schema->hasTable('openregister_sources')) { $table = $schema->createTable('openregister_sources'); - $table->addColumn('id', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('title', Types::STRING, ['notnull' => true, 'length' => 255]); $table->addColumn('description', Types::TEXT, ['notnull' => false]); $table->addColumn('database_url', Types::STRING, ['notnull' => true, 'length' => 255]); $table->addColumn('type', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'register_sources_title_index'); @@ -62,32 +62,44 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('summary', Types::TEXT, ['notnull' => false]); $table->addColumn('required', Types::JSON, ['notnull' => false]); $table->addColumn('properties', Types::JSON, ['notnull' => false]); - $table->addColumn('archive', Types::JSON, ['notnull' => false]); - $table->addColumn('source', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'register_schemas_title_index'); - $table->addIndex(['source'], 'register_schemas_source_index'); } if (!$schema->hasTable('openregister_registers')) { $table = $schema->createTable('openregister_registers'); - $table->addColumn('id', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]); $table->addColumn('title', Types::STRING, ['notnull' => true, 'length' => 255]); $table->addColumn('description', Types::TEXT, ['notnull' => false]); $table->addColumn('schemas', Types::JSON, ['notnull' => false]); $table->addColumn('source', Types::STRING, ['notnull' => true, 'length' => 64]); $table->addColumn('table_prefix', Types::STRING, ['notnull' => true, 'length' => 64]); - $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); - $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'registers_title_index'); $table->addIndex(['source'], 'registers_source_index'); } + if (!$schema->hasTable('openregister_objects')) { + $table = $schema->createTable('openregister_objects'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]); + $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('register', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('schema', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('object', Types::JSON, ['notnull' => false]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->setPrimaryKey(['id']); + $table->addIndex(['uuid'], 'object_entity_uuid'); + $table->addIndex(['register'], 'object_entity_register'); + $table->addIndex(['schema'], 'object_entity_schema'); + } + return $schema; } diff --git a/lib/Service/MongoDbService.php b/lib/Service/MongoDbService.php new file mode 100755 index 0000000..1e307de --- /dev/null +++ b/lib/Service/MongoDbService.php @@ -0,0 +1,219 @@ + 'objects', + 'collection' => 'json', + ]; + + /** + * Gets a guzzle client based upon given config. + * + * @param array $config The config to be used for the client. + * @return Client + */ + public function getClient(array $config): Client + { + $guzzleConf = $config; + unset($guzzleConf['mongodbCluster']); + + return new Client($config); + } + + /** + * 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 + */ + public function saveObject(array $data, array $config): array + { + $client = $this->getClient(config: $config); + + $object = self::BASE_OBJECT; + $object['dataSource'] = $config['mongodbCluster']; + $object['document'] = $data; + $object['document']['id'] = $object['document']['_id'] = Uuid::v4(); + + $result = $client->post( + uri: 'action/insertOne', + options: ['json' => $object], + ); + $resultData = json_decode( + json: $result->getBody()->getContents(), + associative: true + ); + $id = $resultData['insertedId']; + + 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. + * + * @return array The objects found for given filters. + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function findObjects(array $filters, array $config): array + { + $client = $this->getClient(config: $config); + + $object = self::BASE_OBJECT; + $object['dataSource'] = $config['mongodbCluster']; + $object['filter'] = $filters; + + // @todo Fix mongodb sort + // if (empty($sort) === false) { + // $object['filter'][] = ['$sort' => $sort]; + // } + + $returnData = $client->post( + uri: 'action/find', + options: ['json' => $object] + ); + + return json_decode( + json: $returnData->getBody()->getContents(), + associative: true + ); + } + + /** + * 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. + * + * @return array The resulting object. + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function findObject(array $filters, array $config): array + { + $client = $this->getClient(config: $config); + + $object = self::BASE_OBJECT; + $object['filter'] = $filters; + $object['dataSource'] = $config['mongodbCluster']; + + $returnData = $client->post( + uri: 'action/findOne', + options: ['json' => $object] + ); + + $result = json_decode( + json: $returnData->getBody()->getContents(), + associative: true + ); + + 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. + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateObject(array $filters, array $update, array $config): array + { + $client = $this->getClient(config: $config); + + $dotUpdate = new Dot($update); + + $object = self::BASE_OBJECT; + $object['filter'] = $filters; + $object['update']['$set'] = $update; + $object['upsert'] = true; + $object['dataSource'] = $config['mongodbCluster']; + + + + $returnData = $client->post( + uri: 'action/updateOne', + options: ['json' => $object] + ); + + return $this->findObject($filters, $config); + } + + /** + * Delete an object according to a filter (id specifically) + * + * @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 + */ + public function deleteObject(array $filters, array $config): array + { + $client = $this->getClient(config: $config); + + $object = self::BASE_OBJECT; + $object['filter'] = $filters; + $object['dataSource'] = $config['mongodbCluster']; + + $returnData = $client->post( + uri: 'action/deleteOne', + options: ['json' => $object] + ); + + return []; + } + + /** + * Aggregates objects for search facets. + * + * @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 + */ + public function aggregateObjects(array $filters, array $pipeline, array $config):array + { + $client = $this->getClient(config: $config); + + $object = self::BASE_OBJECT; + $object['filter'] = $filters; + $object['pipeline'] = $pipeline; + $object['dataSource'] = $config['mongodbCluster']; + + $returnData = $client->post( + uri: 'action/aggregate', + options: ['json' => $object] + ); + + return json_decode( + json: $returnData->getBody()->getContents(), + associative: true + ); + + } + +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 8e257d0..2911d17 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,218 +2,279 @@ namespace OCA\OpenRegister\Service; -use Adbar\Dot; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\ObjectEntityMapper; use Symfony\Component\Uid\Uuid; class ObjectService { - - public const BASE_OBJECT = [ - 'database' => 'objects', - 'collection' => 'json', - ]; + private $callLogMapper; /** - * Gets a guzzle client based upon given config. + * The constructor sets al needed variables. * - * @param array $config The config to be used for the client. - * @return Client + * @param ObjectEntityMapper $objectEntityMapper The ObjectEntity Mapper */ - public function getClient(array $config): Client + public function __construct(ObjectEntityMapper $objectEntityMapper, RegisterMapper $registerMapper, SchemaMapper $schemaMapper) { - $guzzleConf = $config; - unset($guzzleConf['mongodbCluster']); - - return new Client($config); + $this->objectEntityMapper = $objectEntityMapper; + $this->registerMapper = $registerMapper; + $this->schemaMapper = $schemaMapper; } /** - * Save an object to MongoDB + * Save an object * - * @param array $data The data to be saved. - * @param array $config The configuration that should be used by the call. + * @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 array The resulting object. - * @throws \GuzzleHttp\Exception\GuzzleException + * @return ObjectEntity The resulting object. */ - public function saveObject(array $data, array $config): array + public function saveObject($register, $schema, array $object): ObjectEntity { - $client = $this->getClient(config: $config); - - $object = self::BASE_OBJECT; - $object['dataSource'] = $config['mongodbCluster']; - $object['document'] = $data; - $object['document']['id'] = $object['document']['_id'] = Uuid::v4(); - - $result = $client->post( - uri: 'action/insertOne', - options: ['json' => $object], - ); - $resultData = json_decode( - json: $result->getBody()->getContents(), - associative: true - ); - $id = $resultData['insertedId']; - - 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. - * - * @return array The objects found for given filters. - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function findObjects(array $filters, array $config): array - { - $client = $this->getClient(config: $config); - - $object = self::BASE_OBJECT; - $object['dataSource'] = $config['mongodbCluster']; - $object['filter'] = $filters; - - // @todo Fix mongodb sort - // if (empty($sort) === false) { - // $object['filter'][] = ['$sort' => $sort]; - // } - - $returnData = $client->post( - uri: 'action/find', - options: ['json' => $object] - ); - - return json_decode( - json: $returnData->getBody()->getContents(), - associative: true - ); + // 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); + } + + // Does the object already exist? + $objectEntity = $this->objectEntityMapper->findByUuid($register, $schema, $object['id']); + + if($objectEntity === null){ + $objectEntity = new ObjectEntity(); + $objectEntity->setRegister($register->getId()); + $objectEntity->setSchema($schema->getId()); + ///return $this->objectEntityMapper->update($objectEntity); + } + + + // Does the object have an if? + if (isset($object['id'])) { + // Update existing object + $objectEntity->setUuid($object['id']); + } else { + // Create new object + $objectEntity->setUuid(Uuid::v4()); + $object['id'] = $objectEntity->getUuid(); + } + + $objectEntity->setObject($object); + + if($objectEntity->getId()){ + return $this->objectEntityMapper->update($objectEntity); + } + return $this->objectEntityMapper->insert($objectEntity); + + //@todo mongodb support + + // Handle external source here if needed + throw new \Exception('Unsupported source type'); } + /** - * 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. + * Get an object * - * @return array The resulting object. + * @param Register $register The register to save the object to. + * @param string $uuid The uuid of the object to get * - * @throws \GuzzleHttp\Exception\GuzzleException + * @return ObjectEntity The resulting object. */ - public function findObject(array $filters, array $config): array + public function getObject(Register $register, string $uuid): ObjectEntity { - $client = $this->getClient(config: $config); + // Lets see if we need to save to an internal source + if ($register->getSource() === 'internal') { + return $this->objectEntityMapper->findByUuid($register,$uuid); + } - $object = self::BASE_OBJECT; - $object['filter'] = $filters; - $object['dataSource'] = $config['mongodbCluster']; + //@todo mongodb support - $returnData = $client->post( - uri: 'action/findOne', - options: ['json' => $object] - ); - - $result = json_decode( - json: $returnData->getBody()->getContents(), - associative: true - ); + // Handle external source here if needed + throw new \Exception('Unsupported source type'); + } - return $result['document']; + /** + * 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, string $uuid) + { + // Lets see if we need to save to an internal source + if ($register->getSource() === 'internal') { + $object = $this->objectEntityMapper->findByUuid($uuid); + $this->objectEntityMapper->delete($object); } + //@todo mongodb support + // Handle external source here if needed + throw new \Exception('Unsupported source type'); + } /** - * 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. + * Gets the appropriate mapper based on the object type. * - * @return array The updated object. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @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 updateObject(array $filters, array $update, array $config): array + public function getMapper(string $objectType) { - $client = $this->getClient(config: $config); - - $dotUpdate = new Dot($update); - - $object = self::BASE_OBJECT; - $object['filter'] = $filters; - $object['update']['$set'] = $update; - $object['upsert'] = true; - $object['dataSource'] = $config['mongodbCluster']; - - - - $returnData = $client->post( - uri: 'action/updateOne', - options: ['json' => $object] - ); - - return $this->findObject($filters, $config); + // 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"); + } } /** - * Delete an object according to a filter (id specifically) + * Gets multiple objects based on the object type and ids. * - * @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 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 deleteObject(array $filters, array $config): array + public function getMultipleObjects(string $objectType, array $ids) { - $client = $this->getClient(config: $config); - - $object = self::BASE_OBJECT; - $object['filter'] = $filters; - $object['dataSource'] = $config['mongodbCluster']; - - $returnData = $client->post( - uri: 'action/deleteOne', - options: ['json' => $object] - ); - - return []; + // 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); } /** - * Aggregates objects for search facets. + * Extends an entity with related objects based on the extend array. * - * @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 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 aggregateObjects(array $filters, array $pipeline, array $config):array + public function extendEntity(array $entity, array $extend): array { - $client = $this->getClient(config: $config); - - $object = self::BASE_OBJECT; - $object['filter'] = $filters; - $object['pipeline'] = $pipeline; - $object['dataSource'] = $config['mongodbCluster']; - - $returnData = $client->post( - uri: 'action/aggregate', - options: ['json' => $object] - ); - - return json_decode( - json: $returnData->getBody()->getContents(), - associative: true - ); - + // 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; + } } diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index ca39445..a5cb602 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -28,13 +28,13 @@ public function mergeFacets(array $existingAggregation, array $newAggregation): $existingAggregationMapped = []; $newAggregationMapped = []; - foreach($existingAggregation as $value) { + foreach ($existingAggregation as $value) { $existingAggregationMapped[$value['_id']] = $value['count']; } - foreach($newAggregation as $value) { - if(isset ($existingAggregationMapped[$value['_id']]) === true) { + foreach ($newAggregation as $value) { + if (isset ($existingAggregationMapped[$value['_id']]) === true) { $newAggregationMapped[$value['_id']] = $existingAggregationMapped[$value['_id']] + $value['count']; } else { $newAggregationMapped[$value['_id']] = $value['count']; @@ -52,13 +52,13 @@ public function mergeFacets(array $existingAggregation, array $newAggregation): private function mergeAggregations(?array $existingAggregations, ?array $newAggregations): array { - if($newAggregations === null) { + if ($newAggregations === null) { return []; } - foreach($newAggregations as $key => $aggregation) { - if(isset($existingAggregations[$key]) === false) { + foreach ($newAggregations as $key => $aggregation) { + if (isset($existingAggregations[$key]) === false) { $existingAggregations[$key] = $aggregation; } else { $existingAggregations[$key] = $this->mergeFacets($existingAggregations[$key], $aggregation); @@ -86,7 +86,7 @@ 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; - if($elasticConfig['location'] !== '') { + if ($elasticConfig['location'] !== '') { $localResults = $this->elasticService->searchObject(filters: $parameters, config: $elasticConfig, totalResults: $totalResults,); } @@ -94,7 +94,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, // $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); - if(count($directory) === 0) { + if (count($directory) === 0) { $pages = (int) ceil($totalResults / $limit); return [ 'results' => $localResults['results'], @@ -114,8 +114,8 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $promises = []; - foreach($directory as $instance) { - if( + foreach ($directory as $instance) { + if ( $instance['default'] === false || isset($parameters['.catalogi']) === true && in_array($instance['catalogId'], $parameters['.catalogi']) === false @@ -128,7 +128,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, unset($parameters['.catalogi']); - foreach($searchEndpoints as $searchEndpoint => $catalogi) { + foreach ($searchEndpoints as $searchEndpoint => $catalogi) { $parameters['_catalogi'] = $catalogi; @@ -137,8 +137,8 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $responses = Utils::settle($promises)->wait(); - foreach($responses as $response) { - if($response['state'] === 'fulfilled') { + foreach ($responses as $response) { + if ($response['state'] === 'fulfilled') { $responseData = json_decode( json: $response['value']->getBody()->getContents(), associative: true @@ -355,12 +355,12 @@ public function parseQueryString (string $queryString = ''): array { $pairs = explode(separator: '&', string: $queryString); - foreach($pairs as $pair) { + foreach ($pairs as $pair) { $kvpair = explode(separator: '=', string: $pair); $key = urldecode(string: $kvpair[0]); $value = ''; - if(count(value: $kvpair) === 2) { + if (count(value: $kvpair) === 2) { $value = urldecode(string: $kvpair[1]); } diff --git a/package-lock.json b/package-lock.json index c082b2f..84deeda 100755 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "remark-preset-lint-consistent": "^6.0.0", "remark-preset-lint-recommended": "^7.0.0", "style-loader": "^3.3.3", + "uuid": "^10.0.0", "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", "vue-loader": "^15.11.1 <16.0.0", @@ -18876,6 +18877,16 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", @@ -21061,11 +21072,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "peer": true, + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -36000,6 +36013,15 @@ "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "peer": true + } } }, "source-map": { @@ -37601,11 +37623,9 @@ "peer": true }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "peer": true + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, "v8-to-istanbul": { "version": "9.3.0", diff --git a/package.json b/package.json index 38887bd..29e5c3a 100755 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "remark-preset-lint-consistent": "^6.0.0", "remark-preset-lint-recommended": "^7.0.0", "style-loader": "^3.3.3", + "uuid": "^10.0.0", "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", "vue-loader": "^15.11.1 <16.0.0", diff --git a/src/dialogs/Dialogs.vue b/src/dialogs/Dialogs.vue index f404fe9..9f24007 100755 --- a/src/dialogs/Dialogs.vue +++ b/src/dialogs/Dialogs.vue @@ -1,7 +1,6 @@ diff --git a/src/modals/object/DeleteObject.vue b/src/modals/object/DeleteObject.vue new file mode 100644 index 0000000..235983d --- /dev/null +++ b/src/modals/object/DeleteObject.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/modals/object/EditObject.vue b/src/modals/object/EditObject.vue new file mode 100644 index 0000000..c5bf92e --- /dev/null +++ b/src/modals/object/EditObject.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/src/modals/register/DeleteRegister.vue b/src/modals/register/DeleteRegister.vue new file mode 100644 index 0000000..7963ccb --- /dev/null +++ b/src/modals/register/DeleteRegister.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/modals/register/EditRegister.vue b/src/modals/register/EditRegister.vue new file mode 100644 index 0000000..6392d53 --- /dev/null +++ b/src/modals/register/EditRegister.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/src/modals/schema/DeleteSchema.vue b/src/modals/schema/DeleteSchema.vue new file mode 100644 index 0000000..080b078 --- /dev/null +++ b/src/modals/schema/DeleteSchema.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/modals/schema/DeleteSchemaProperty.vue b/src/modals/schema/DeleteSchemaProperty.vue new file mode 100644 index 0000000..4caf685 --- /dev/null +++ b/src/modals/schema/DeleteSchemaProperty.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/src/modals/schema/EditSchema.vue b/src/modals/schema/EditSchema.vue new file mode 100644 index 0000000..a1296fd --- /dev/null +++ b/src/modals/schema/EditSchema.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/modals/schema/EditSchemaProperty.vue b/src/modals/schema/EditSchemaProperty.vue new file mode 100644 index 0000000..ebdc957 --- /dev/null +++ b/src/modals/schema/EditSchemaProperty.vue @@ -0,0 +1,497 @@ + + + + + + diff --git a/src/modals/source/DeleteSource.vue b/src/modals/source/DeleteSource.vue new file mode 100644 index 0000000..54fc67a --- /dev/null +++ b/src/modals/source/DeleteSource.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/modals/source/EditSource.vue b/src/modals/source/EditSource.vue new file mode 100644 index 0000000..6b6cac9 --- /dev/null +++ b/src/modals/source/EditSource.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue index 231b459..714b48e 100755 --- a/src/navigation/MainMenu.vue +++ b/src/navigation/MainMenu.vue @@ -20,7 +20,13 @@ import { navigationStore } from '../store/store.js' + + + + diff --git a/src/views/schema/SchemaDetails.vue b/src/views/schema/SchemaDetails.vue index 5d327f1..3a303ee 100644 --- a/src/views/schema/SchemaDetails.vue +++ b/src/views/schema/SchemaDetails.vue @@ -8,10 +8,10 @@ import { schemaStore, navigationStore } from '../../store/store.js'

- {{ schemaStore.schemaItem.name }} + {{ schemaStore.schemaItem.title }}

- + @@ -19,13 +19,19 @@ import { schemaStore, navigationStore } from '../../store/store.js' - Bewerken + Edit + + + + Add Property - Verwijderen + Delete
@@ -33,27 +39,94 @@ import { schemaStore, navigationStore } from '../../store/store.js'
- Status: -

{{ schemaStore.schemaItem.status }}

+ Version: +

{{ schemaStore.schemaItem.version }}

+
+ + +
+ + + + + +
+
+ No properties found +
+
+ +
+ + + + +
+
+ No logs found +
+
+
+
diff --git a/src/views/source/SourcesIndex.vue b/src/views/source/SourcesIndex.vue index 64bf709..9e97f6f 100644 --- a/src/views/source/SourcesIndex.vue +++ b/src/views/source/SourcesIndex.vue @@ -10,14 +10,14 @@ import { sourceStore, navigationStore } from '../../store/store.js' @@ -67,10 +67,10 @@ import { sourceStore, navigationStore, searchStore } from '../../store/store.js' class="loadingIcon" :size="64" appearance="dark" - name="Bronnen aan het laden" /> + name="Loading sources" />
- Er zijn nog geen bronnen gedefinieerd. + No sources have been defined yet.