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 f1066b32c9..424fe67741 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/ObjectEntity.php b/lib/Db/ObjectEntity.php new file mode 100644 index 0000000000..2035c417fd --- /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 + ]; + } +} \ No newline at end of file diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php new file mode 100644 index 0000000000..a7e77a7d36 --- /dev/null +++ b/lib/Db/ObjectEntityMapper.php @@ -0,0 +1,132 @@ +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 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(); + + $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 f13c802c22..61b9432ff0 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -88,6 +88,21 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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 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..e8ac3c31ff 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,218 +2,104 @@ 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', - ]; - - /** - * 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); - } + private $callLogMapper; /** - * Finds objects based upon a set of filters. + * The constructor sets al needed variables. * - * @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 + * @param ObjectEntityMapper $objectEntityMapper The ObjectEntity Mapper */ - public function findObjects(array $filters, array $config): array + public function __construct(ObjectEntityMapper $objectEntityMapper) { - $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 - ); + $this->objectEntityMapper = $objectEntityMapper; } /** - * 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. + * Save an object * - * @return array The resulting object. + * @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. * - * @throws \GuzzleHttp\Exception\GuzzleException + * @return ObjectEntity The resulting object. */ - public function findObject(array $filters, array $config): array + public function saveObject(Register $register, Schema $schema, array $object): ObjectEntity { - $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']; + // 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); + } + } + + //@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. + * Get an object * - * @return array The updated 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']; + // Lets see if we need to save to an internal source + if ($register->getSource() === 'internal') { + return $this->objectEntityMapper->findByUuid($uuid); + } + //@todo mongodb support - - $returnData = $client->post( - uri: 'action/updateOne', - options: ['json' => $object] - ); - - return $this->findObject($filters, $config); - } - + // 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'); + } } 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..fb6e45f7c0 --- /dev/null +++ b/src/entities/object/object.mock.ts @@ -0,0 +1,25 @@ +import { ObjectEntity } 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 ObjectEntity(item)) diff --git a/src/entities/object/object.spec.ts b/src/entities/object/object.spec.ts new file mode 100644 index 0000000000..d9232f9ef6 --- /dev/null +++ b/src/entities/object/object.spec.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ObjectEntity } from './object' +import { mockObjectData } from './object.mock' + +describe('Object Entity', () => { + it('should create an Object entity with full data', () => { + const object = new ObjectEntity(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 ObjectEntity(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 ObjectEntity(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..a78523ed1e --- /dev/null +++ b/src/entities/object/object.ts @@ -0,0 +1,38 @@ +import { SafeParseReturnType, z } from 'zod' +import { TObject } from './object.types' + +export class ObjectEntity 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/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, }, } 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 @@ + + + + + + +