From 11eaf9f468f84ea4c1ba18a696aad09cbd41734a Mon Sep 17 00:00:00 2001 From: Thijn Date: Fri, 27 Sep 2024 13:49:51 +0200 Subject: [PATCH 1/5] applied fixes from REG-10 to working pages this is so I can pull these fixes to other issues without having to wait on REGISTER-10 to be merged --- lib/Db/Register.php | 2 +- lib/Db/Source.php | 10 +++++----- lib/Migration/Version1Date20240924200009.php | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/Db/Register.php b/lib/Db/Register.php index 68ad1028be..6cf83515ea 100644 --- a/lib/Db/Register.php +++ b/lib/Db/Register.php @@ -19,7 +19,7 @@ class Register extends Entity implements JsonSerializable public function __construct() { $this->addType(fieldName: 'title', type: 'string'); $this->addType(fieldName: 'description', type: 'string'); - $this->addType(fieldName: 'properties', type: 'json'); + $this->addType(fieldName: 'schemas', type: 'json'); $this->addType(fieldName: 'source', type: 'string'); $this->addType(fieldName: 'tablePrefix', type: 'string'); $this->addType(fieldName:'updated', type: 'datetime'); diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 017285b72a..9203b6e253 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -17,11 +17,11 @@ class Source extends Entity implements JsonSerializable public function __construct() { $this->addType(fieldName: 'title', type: 'string'); - $this->addType('description', 'string'); - $this->addType('databaseUrl', 'string'); - $this->addType('type', 'string'); - $this->addType('updated', 'datetime'); - $this->addType('created', 'datetime'); + $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 diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index 0d950fdb47..f13c802c22 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -45,8 +45,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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'); @@ -64,8 +64,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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'); @@ -80,8 +80,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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'); From 824fd8418de17ca8329d35727c076ec09906db09 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 09:37:08 +0200 Subject: [PATCH 2/5] Add save object to the object service --- lib/Db/ObjectEntity.php | 73 +++++++ lib/Db/ObjectEntityMapper.php | 74 +++++++ lib/Migration/Version1Date20240924200009.php | 22 +- lib/Service/MongoDbService.php | 219 +++++++++++++++++++ lib/Service/ObjectService.php | 80 ++++--- 5 files changed, 424 insertions(+), 44 deletions(-) create mode 100644 lib/Db/ObjectEntity.php create mode 100644 lib/Db/ObjectEntityMapper.php create mode 100755 lib/Service/MongoDbService.php diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php new file mode 100644 index 0000000000..14473c9583 --- /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' => $this->updated, + 'created' => $this->created + ]; + } +} \ No newline at end of file diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php new file mode 100644 index 0000000000..2d113769d9 --- /dev/null +++ b/lib/Db/ObjectEntityMapper.php @@ -0,0 +1,74 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity(query: $qb); + } + + 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): Object + { + $obj = new Object(); + $obj->hydrate(object: $object); + return $this->insert(entity: $obj); + } + + public function updateFromArray(int $id, array $object): Object + { + $obj = $this->find($id); + $obj->hydrate($object); + + return $this->update($obj); + } +} diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index 0d950fdb47..a0a9b7de08 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -79,15 +79,31 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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('table_prefix', Types::STRING, ['notnull' => true, 'length' => 64, 'default' => 'internal']); + $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_object_entity')) { + $table = $schema->createTable('openregister_object_entity'); + $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 0000000000..1e307dea2e --- /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 8e257d00e1..cf473edc95 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,62 +2,60 @@ namespace OCA\OpenRegister\Service; -use Adbar\Dot; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; -use Symfony\Component\Uid\Uuid; +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; 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) { - $guzzleConf = $config; - unset($guzzleConf['mongodbCluster']); - - return new Client($config); + $this->objectEntityMapper = $objectEntityMapper; } /** - * 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 $register The data to be saved. + * @param Schema $schema The data to be saved. + * @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 $register, Schema $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); + // Lets see if we need to save to an internal source + if ($register->getSource() === 'internal') { + $objectEntity = new ObjectEntity(); + $objectEntity->setRegister($register->getId()); + $objectEntity->setSchema($schema->getId()); + $objectEntity->setObject($object); + + if (isset($object['id'])) { + // Update existing object + $objectEntity->setUuidId($object['id']); + return $this->objectEntityMapper->update($objectEntity); + } else { + // Create new object + $objectEntity->setUuidId(Uuid::v4()); + return $this->objectEntityMapper->insert($objectEntity); + } + } + + // Handle external source here if needed + throw new \Exception('Unsupported source type'); } /** From db4e25ed60c30b647d4dd89d8d730e54f088c665 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 10:51:03 +0200 Subject: [PATCH 3/5] Bit more work on object service --- appinfo/routes.php | 4 +- lib/Controller/ObjectsController.php | 165 +++++++++++++++++ lib/Controller/RegistersController.php | 22 ++- lib/Db/ObjectEntityMapper.php | 58 ++++++ lib/Migration/Version1Date20240924200009.php | 5 +- lib/Service/ObjectService.php | 184 ++++--------------- 6 files changed, 285 insertions(+), 153 deletions(-) create mode 100644 lib/Controller/ObjectsController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 6cba70ca68..93948ea16d 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/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php new file mode 100644 index 0000000000..9cc68026bd --- /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 782ed80a0f..483a045215 100644 --- a/lib/Controller/RegistersController.php +++ b/lib/Controller/RegistersController.php @@ -6,6 +6,7 @@ use OCA\OpenRegister\Service\SearchService; 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; @@ -25,7 +26,8 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly RegisterMapper $registerMapper + private readonly RegisterMapper $registerMapper, + private readonly ObjectEntityMapper $objectEntityMapper ) { parent::__construct($appName, $request); @@ -162,4 +164,22 @@ public function destroy(int $id): JSONResponse 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/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 2d113769d9..a7e77a7d36 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -15,6 +15,12 @@ public function __construct(IDBConnection $db) parent::__construct($db, 'openregister_objects'); } + /** + * Find an object by ID + * + * @param int $id The ID of the object to find + * @return Object The object + */ public function find(int $id): Object { $qb = $this->db->getQueryBuilder(); @@ -28,6 +34,58 @@ public function find(int $id): Object return $this->findEntity(query: $qb); } + /** + * Find an object by UUID + * + * @param string $uuid The UUID of the object to find + * @return Object The object + */ + public function findByUuid(string $uuid): Object + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('uuid', $qb->createNamedParameter($uuid)) + ); + + return $this->findEntity(query: $qb); + } + + /** + * 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 objects + */ + public function findByRegisterAndSchema(string $register, string $schema): Object + { + $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 objects + * + * @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 objects + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index ab29fb2116..61b9432ff0 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -88,8 +88,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['source'], 'registers_source_index'); } - if (!$schema->hasTable('openregister_object_entity')) { - $table = $schema->createTable('openregister_object_entity'); + 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]); @@ -103,7 +103,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['schema'], 'object_entity_schema'); } - return $schema; } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index cf473edc95..e8ac3c31ff 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -28,8 +28,8 @@ public function __construct(ObjectEntityMapper $objectEntityMapper) /** * Save an object * - * @param Register $register The data to be saved. - * @param Schema $schema The data to be saved. + * @param Register $register The register to save the object to. + * @param Schema $schema The schema to save the object to. * @param array $object The data to be saved. * * @return ObjectEntity The resulting object. @@ -54,164 +54,52 @@ public function saveObject(Register $register, Schema $schema, array $object): O } } + //@todo mongodb support + // Handle external source here if needed throw new \Exception('Unsupported source type'); } /** - * 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. + * Get an object * - * @throws \GuzzleHttp\Exception\GuzzleException + * @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 updateObject(array $filters, array $update, array $config): array + public function getObject(Register $register, string $uuid): ObjectEntity { - $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] - ); + // Lets see if we need to save to an internal source + if ($register->getSource() === 'internal') { + return $this->objectEntityMapper->findByUuid($uuid); + } - return $this->findObject($filters, $config); - } + //@todo mongodb support + // Handle external source here if needed + throw new \Exception('Unsupported source type'); + } + /** - * 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 []; + * 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); } - /** - * 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 - ); - - } + //@todo mongodb support + // Handle external source here if needed + throw new \Exception('Unsupported source type'); + } } From c11ec59fbf67092522822586ec1077784c80e656 Mon Sep 17 00:00:00 2001 From: Thijn Date: Mon, 30 Sep 2024 14:30:09 +0200 Subject: [PATCH 4/5] added object pages, store and entity --- lib/Db/ObjectEntity.php | 4 +- src/entities/index.js | 1 + src/entities/object/index.js | 3 + src/entities/object/object.mock.ts | 25 +++++ src/entities/object/object.spec.ts | 38 ++++++++ src/entities/object/object.ts | 38 ++++++++ src/entities/object/object.types.ts | 9 ++ src/store/modules/object.js | 145 ++++++++++++++++++++++++++++ src/store/modules/object.spec.js | 36 +++++++ src/store/store.js | 4 +- src/views/object/ObjectDetails.vue | 97 +++++++++++++++++++ src/views/object/ObjectsIndex.vue | 46 +++++++++ src/views/object/ObjectsList.vue | 111 +++++++++++++++++++++ 13 files changed, 554 insertions(+), 3 deletions(-) create mode 100644 src/entities/object/index.js create mode 100644 src/entities/object/object.mock.ts create mode 100644 src/entities/object/object.spec.ts create mode 100644 src/entities/object/object.ts create mode 100644 src/entities/object/object.types.ts create mode 100644 src/store/modules/object.js create mode 100644 src/store/modules/object.spec.js create mode 100644 src/views/object/ObjectDetails.vue create mode 100644 src/views/object/ObjectsIndex.vue create mode 100644 src/views/object/ObjectsList.vue diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php index 14473c9583..2035c417fd 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -66,8 +66,8 @@ public function jsonSerialize(): array 'register' => $this->register, 'schema' => $this->schema, 'object' => $this->object, - '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/src/entities/index.js b/src/entities/index.js index 66c19b99b3..5bda5a30a6 100755 --- a/src/entities/index.js +++ b/src/entities/index.js @@ -2,3 +2,4 @@ export * from './schema/index.js' export * from './register/index.js' export * from './source/index.js' +export * from './object/index.js' diff --git a/src/entities/object/index.js b/src/entities/object/index.js new file mode 100644 index 0000000000..ca4a7b67a8 --- /dev/null +++ b/src/entities/object/index.js @@ -0,0 +1,3 @@ +export * from './object.ts' +export * from './object.types.ts' +export * from './object.mock.ts' diff --git a/src/entities/object/object.mock.ts b/src/entities/object/object.mock.ts new file mode 100644 index 0000000000..6968c7e30d --- /dev/null +++ b/src/entities/object/object.mock.ts @@ -0,0 +1,25 @@ +import { Object } from './object' +import { TObject } from './object.types' + +export const mockObjectData = (): TObject[] => [ + { + id: '1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + uuid: 'uuid-1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + register: 'Character Register', + schema: 'character_schema', + object: JSON.stringify({ key: 'value' }), + created: new Date().toISOString(), + updated: new Date().toISOString(), + }, + { + id: '5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + uuid: 'uuid-5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + register: 'Item Register', + schema: 'item_schema', + object: JSON.stringify({ key: 'value' }), + created: new Date().toISOString(), + updated: new Date().toISOString(), + }, +] + +export const mockObject = (data: TObject[] = mockObjectData()): TObject[] => data.map(item => new Object(item)) diff --git a/src/entities/object/object.spec.ts b/src/entities/object/object.spec.ts new file mode 100644 index 0000000000..220cb474f5 --- /dev/null +++ b/src/entities/object/object.spec.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Object } from './object' +import { mockObjectData } from './object.mock' + +describe('Object Entity', () => { + it('should create an Object entity with full data', () => { + const object = new Object(mockObjectData()[0]) + + expect(object).toBeInstanceOf(Object) + expect(object).toEqual(mockObjectData()[0]) + expect(object.validate().success).toBe(true) + }) + + it('should create an Object entity with partial data', () => { + const object = new Object(mockObjectData()[0]) + + expect(object).toBeInstanceOf(Object) + expect(object.id).toBe('') + expect(object.uuid).toBe(mockObjectData()[0].uuid) + expect(object.register).toBe(mockObjectData()[0].register) + expect(object.schema).toBe(mockObjectData()[0].schema) + expect(object.object).toBe(mockObjectData()[0].object) + expect(object.updated).toBe(mockObjectData()[0].updated) + expect(object.created).toBe(mockObjectData()[0].created) + expect(object.validate().success).toBe(true) + }) + + it('should fail validation with invalid data', () => { + const object = new Object(mockObjectData()[1]) + + expect(object).toBeInstanceOf(Object) + expect(object.validate().success).toBe(false) + expect(object.validate().error?.issues).toContainEqual(expect.objectContaining({ + path: ['id'], + message: 'String must contain at least 1 character(s)', + })) + }) +}) diff --git a/src/entities/object/object.ts b/src/entities/object/object.ts new file mode 100644 index 0000000000..70ce6fd59e --- /dev/null +++ b/src/entities/object/object.ts @@ -0,0 +1,38 @@ +import { SafeParseReturnType, z } from 'zod' +import { TObject } from './object.types' + +export class Object implements TObject { + + public id: string + public uuid: string + public register: string + public schema: string + public object: string + public updated: string + public created: string + + constructor(object: TObject) { + this.id = object.id || '' + this.uuid = object.uuid + this.register = object.register + this.schema = object.schema + this.object = object.object + this.updated = object.updated || '' + this.created = object.created || '' + } + + public validate(): SafeParseReturnType { + const schema = z.object({ + id: z.string().min(1), + uuid: z.string().min(1), + register: z.string().min(1), + schema: z.string().min(1), + object: z.string(), + updated: z.string(), + created: z.string(), + }) + + return schema.safeParse(this) + } + +} diff --git a/src/entities/object/object.types.ts b/src/entities/object/object.types.ts new file mode 100644 index 0000000000..4aa6ea226d --- /dev/null +++ b/src/entities/object/object.types.ts @@ -0,0 +1,9 @@ +export type TObject = { + id?: string + uuid: string + register: string + schema: string + object: string // JSON object + updated: string + created: string +} diff --git a/src/store/modules/object.js b/src/store/modules/object.js new file mode 100644 index 0000000000..f90d0aae04 --- /dev/null +++ b/src/store/modules/object.js @@ -0,0 +1,145 @@ +/* eslint-disable no-console */ +import { defineStore } from 'pinia' +import { ObjectEntity } from '../../entities/index.js' + +export const useObjectStore = defineStore('object', { + state: () => ({ + objectItem: false, + objectList: [], + }), + actions: { + setObjectItem(objectItem) { + this.objectItem = objectItem && new ObjectEntity(objectItem) + console.log('Active object item set to ' + objectItem) + }, + setObjectList(objectList) { + this.objectList = objectList.map( + (objectItem) => new ObjectEntity(objectItem), + ) + console.log('Object list set to ' + objectList.length + ' items') + }, + /* istanbul ignore next */ // ignore this for Jest until moved into a service + async refreshObjectList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/openregister/api/objects' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch(endpoint, { + method: 'GET', + }) + .then( + (response) => { + response.json().then( + (data) => { + this.setObjectList(data.results) + }, + ) + }, + ) + .catch( + (err) => { + console.error(err) + }, + ) + }, + // New function to get a single object + async getObject(id) { + const endpoint = `/index.php/apps/openregister/api/objects/${id}` + try { + const response = await fetch(endpoint, { + method: 'GET', + }) + const data = await response.json() + this.setObjectItem(data) + return data + } catch (err) { + console.error(err) + throw err + } + }, + // Delete an object + async deleteObject(objectItem) { + if (!objectItem.id) { + throw new Error('No object item to delete') + } + + console.log('Deleting object...') + + const endpoint = `/index.php/apps/openregister/api/objects/${objectItem.id}` + + try { + const response = await fetch(endpoint, { + method: 'DELETE', + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const responseData = await response.json() + + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response data') + } + + this.refreshObjectList() + + return { response, data: responseData } + } catch (error) { + console.error('Error deleting object:', error) + throw new Error(`Failed to delete object: ${error.message}`) + } + }, + // Create or save an object from store + async saveObject(objectItem) { + if (!objectItem) { + throw new Error('No object item to save') + } + + console.log('Saving object...') + + const isNewObject = !objectItem.id + const endpoint = isNewObject + ? '/index.php/apps/openregister/api/objects' + : `/index.php/apps/openregister/api/objects/${objectItem.id}` + const method = isNewObject ? 'POST' : 'PUT' + + // change updated to current date as a singular iso date string + objectItem.updated = new Date().toISOString() + + try { + const response = await fetch( + endpoint, + { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(objectItem), + }, + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const responseData = await response.json() + + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response data') + } + + const data = new ObjectEntity(responseData) + + this.setObjectItem(data) + this.refreshObjectList() + + return { response, data } + } catch (error) { + console.error('Error saving object:', error) + throw new Error(`Failed to save object: ${error.message}`) + } + }, + }, +}) diff --git a/src/store/modules/object.spec.js b/src/store/modules/object.spec.js new file mode 100644 index 0000000000..53c4c969aa --- /dev/null +++ b/src/store/modules/object.spec.js @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import { setActivePinia, createPinia } from 'pinia' + +import { useObjectStore } from './object.js' +import { ObjectEntity, mockObject } from '../../entities/index.js' + +describe('Object Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('sets object item correctly', () => { + const store = useObjectStore() + + store.setObjectItem(mockObject()[0]) + + expect(store.objectItem).toBeInstanceOf(ObjectEntity) + expect(store.objectItem).toEqual(mockObject()[0]) + + expect(store.objectItem.validate().success).toBe(true) + }) + + it('sets object list correctly', () => { + const store = useObjectStore() + + store.setObjectList(mockObject()) + + expect(store.objectList).toHaveLength(mockObject().length) + + store.objectList.forEach((item, index) => { + expect(item).toBeInstanceOf(ObjectEntity) + expect(item).toEqual(mockObject()[index]) + expect(item.validate().success).toBe(true) + }) + }) +}) diff --git a/src/store/store.js b/src/store/store.js index 0790271f93..6c73100b29 100755 --- a/src/store/store.js +++ b/src/store/store.js @@ -6,13 +6,14 @@ import { useSearchStore } from './modules/search.js' import { useRegisterStore } from './modules/register.js' import { useSourceStore } from './modules/source.js' import { useSchemaStore } from './modules/schema.js' +import { useObjectStore } from './modules/object.js' const navigationStore = useNavigationStore(pinia) const searchStore = useSearchStore(pinia) const registerStore = useRegisterStore(pinia) const sourceStore = useSourceStore(pinia) const schemaStore = useSchemaStore(pinia) - +const objectStore = useObjectStore(pinia) export { // generic navigationStore, @@ -20,4 +21,5 @@ export { registerStore, sourceStore, schemaStore, + objectStore, } diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue new file mode 100644 index 0000000000..2a1fe8a6c6 --- /dev/null +++ b/src/views/object/ObjectDetails.vue @@ -0,0 +1,97 @@ + + + + + + + diff --git a/src/views/object/ObjectsIndex.vue b/src/views/object/ObjectsIndex.vue new file mode 100644 index 0000000000..befab1ade6 --- /dev/null +++ b/src/views/object/ObjectsIndex.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/views/object/ObjectsList.vue b/src/views/object/ObjectsList.vue new file mode 100644 index 0000000000..68a8f37655 --- /dev/null +++ b/src/views/object/ObjectsList.vue @@ -0,0 +1,111 @@ + + + + + + + From 5c8c5af0e607fc8891d37f0bc68e94b23b3eb55c Mon Sep 17 00:00:00 2001 From: Thijn Date: Mon, 30 Sep 2024 14:59:34 +0200 Subject: [PATCH 5/5] linked pages to page controller --- src/entities/object/object.mock.ts | 4 ++-- src/entities/object/object.spec.ts | 8 ++++---- src/entities/object/object.ts | 2 +- src/navigation/MainMenu.vue | 6 ++++++ src/views/Views.vue | 4 +++- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/entities/object/object.mock.ts b/src/entities/object/object.mock.ts index 6968c7e30d..fb6e45f7c0 100644 --- a/src/entities/object/object.mock.ts +++ b/src/entities/object/object.mock.ts @@ -1,4 +1,4 @@ -import { Object } from './object' +import { ObjectEntity } from './object' import { TObject } from './object.types' export const mockObjectData = (): TObject[] => [ @@ -22,4 +22,4 @@ export const mockObjectData = (): TObject[] => [ }, ] -export const mockObject = (data: TObject[] = mockObjectData()): TObject[] => data.map(item => new Object(item)) +export const mockObject = (data: TObject[] = mockObjectData()): TObject[] => data.map(item => new ObjectEntity(item)) diff --git a/src/entities/object/object.spec.ts b/src/entities/object/object.spec.ts index 220cb474f5..d9232f9ef6 100644 --- a/src/entities/object/object.spec.ts +++ b/src/entities/object/object.spec.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Object } from './object' +import { ObjectEntity } from './object' import { mockObjectData } from './object.mock' describe('Object Entity', () => { it('should create an Object entity with full data', () => { - const object = new Object(mockObjectData()[0]) + const object = new ObjectEntity(mockObjectData()[0]) expect(object).toBeInstanceOf(Object) expect(object).toEqual(mockObjectData()[0]) @@ -12,7 +12,7 @@ describe('Object Entity', () => { }) it('should create an Object entity with partial data', () => { - const object = new Object(mockObjectData()[0]) + const object = new ObjectEntity(mockObjectData()[0]) expect(object).toBeInstanceOf(Object) expect(object.id).toBe('') @@ -26,7 +26,7 @@ describe('Object Entity', () => { }) it('should fail validation with invalid data', () => { - const object = new Object(mockObjectData()[1]) + const object = new ObjectEntity(mockObjectData()[1]) expect(object).toBeInstanceOf(Object) expect(object.validate().success).toBe(false) diff --git a/src/entities/object/object.ts b/src/entities/object/object.ts index 70ce6fd59e..a78523ed1e 100644 --- a/src/entities/object/object.ts +++ b/src/entities/object/object.ts @@ -1,7 +1,7 @@ import { SafeParseReturnType, z } from 'zod' import { TObject } from './object.types' -export class Object implements TObject { +export class ObjectEntity implements TObject { public id: string public uuid: string diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue index 231b459041..dc08bd786a 100755 --- a/src/navigation/MainMenu.vue +++ b/src/navigation/MainMenu.vue @@ -20,7 +20,13 @@ import { navigationStore } from '../store/store.js' + + + + @@ -20,7 +21,7 @@ import Dashboard from './dashboard/DashboardIndex.vue' import RegistersIndex from './register/RegistersIndex.vue' import SourcesIndex from './source/SourcesIndex.vue' import SchemasIndex from './schema/SchemasIndex.vue' - +import ObjectsIndex from './object/ObjectsIndex.vue' export default { name: 'Views', components: { @@ -29,6 +30,7 @@ export default { RegistersIndex, SourcesIndex, SchemasIndex, + ObjectsIndex, }, }