diff --git a/appinfo/routes.php b/appinfo/routes.php index 307a966..63b8637 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -13,6 +13,7 @@ ['name' => 'objects#logs', 'url' => '/api/objects-logs/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'objects#mappings', 'url' => '/api/objects/mappings', 'verb' => 'GET'], ['name' => 'objects#auditTrails', 'url' => '/api/objects/audit-trails/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + ['name' => 'objects#relations', 'url' => '/api/objects/relations/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'], ['name' => 'schemas#uploadUpdate', 'url' => '/api/schemas/{id}/upload', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index f5fc177..bc8c25f 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -180,7 +180,7 @@ public function create(ObjectService $objectService): JSONResponse * * @return JSONResponse A JSON response containing the updated object details */ - public function update(int $id): JSONResponse + public function update(int $id, ObjectService $objectService): JSONResponse { $data = $this->request->getParams(); $object = $data['object']; @@ -204,8 +204,12 @@ public function update(int $id): JSONResponse } // save it - $oldObject = $this->objectEntityMapper->find($id); - $objectEntity = $this->objectEntityMapper->updateFromArray(id: $id, object: $data); + try { + $objectEntity = $objectService->saveObject(register: $data['register'], schema: $data['schema'], object: $data['object']); + } catch (ValidationException $exception) { + $formatter = new ErrorFormatter(); + return new JSONResponse(['message' => $exception->getMessage(), 'validationErrors' => $formatter->format($exception->getErrors())], 400); + } $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); @@ -274,6 +278,36 @@ public function contracts(int $id): JSONResponse return new JSONResponse(['error' => 'Not yet implemented'], 501); } + /** + * Retrieves all objects that use a object + * + * This method returns all the call logs associated with a object based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to retrieve logs for + * + * @return JSONResponse A JSON response containing the call logs + */ + public function relations(int $id): JSONResponse + { + try { + // Lets grap the object to stablish an uri + $object = $this->objectEntityMapper->find($id); + $relations = $this->objectEntityMapper->findByRelationUri($object->getUri()); + + // We dont want to return the entity, but the object (and kant reley on the normal serilzier) + foreach ($relations as $key => $relation) { + $relations[$key] = $relation->getObjectArray(); + } + + return new JSONResponse($relations); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Relations not found'], 404); + } + } + /** * Retrieves call logs for an object * diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php index 269e737..0cd7250 100644 --- a/lib/Controller/SchemasController.php +++ b/lib/Controller/SchemasController.php @@ -219,6 +219,8 @@ public function upload(?int $id = null): JSONResponse return $phpArray; } + //@todo Maybe check if Schema already exists? If uploaded with url, check if schema with this $phpArray['source'] exists? + // Set default title if not provided or empty if (empty($phpArray['title']) === true) { $phpArray['title'] = 'New Schema'; @@ -236,43 +238,30 @@ public function upload(?int $id = null): JSONResponse /** * Creates and return a json file for a Schema. - * @todo move most of this code to DownloadService and make it even more Abstract using Entity->jsonSerialize instead of Schema->jsonSerialize, etc. * * @NoAdminRequired * @NoCSRFRequired * * @param int $id The ID of the schema to return json file for * @return JSONResponse A json Response containing the json + * @throws \Exception */ public function download(int $id): JSONResponse { - try { - $schema = $this->schemaMapper->find($id); - } catch (DoesNotExistException $exception) { - return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); - } - - $contentType = $this->request->getHeader('Content-Type'); + $accept = $this->request->getHeader('Accept'); - if (empty($contentType) === true) { - return new JSONResponse(data: ['error' => 'Request is missing header Content-Type'], statusCode: 400); + if (empty($accept) === true) { + return new JSONResponse(data: ['error' => 'Request is missing header Accept'], statusCode: 400); } - switch ($contentType) { - case 'application/json': - $type = 'json'; - $responseData = [ - 'jsonArray' => $schema->jsonSerialize(), - 'jsonString' => json_encode($schema->jsonSerialize()) - ]; - break; - default: - return new JSONResponse(data: ['error' => "The Content-Type $contentType is not supported."], statusCode: 400); - } + $responseData = $this->downloadService->download(objectType: 'schema', id: $id, accept: $accept); - // @todo Create a downloadable json file and return it. - $file = $this->downloadService->download(type: $type); + $statusCode = 200; + if (isset($responseData['statusCode']) === true) { + $statusCode = $responseData['statusCode']; + unset($responseData['statusCode']); + } - return new JSONResponse($responseData); + return new JSONResponse(data: $responseData, statusCode: $statusCode); } } diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php index 2b27355..f31fdaf 100644 --- a/lib/Db/ObjectAuditLogMapper.php +++ b/lib/Db/ObjectAuditLogMapper.php @@ -76,9 +76,12 @@ public function updateFromArray(int $id, array $object): ObjectAuditLog $obj->hydrate($object); // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if (isset($object['version']) === false) { + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + } + return $this->update($obj); } diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index a64af65..860523d 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -181,12 +181,36 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters } } + // @roto: tody this code up please and make ik monogdb compatible + // Check if _relations filter exists to search in relations column + if (isset($filters['_relations']) === true) { + // Handle both single string and array of relations + $relations = (array) $filters['_relations']; + + // Build OR conditions for each relation + $orConditions = []; + foreach ($relations as $relation) { + $orConditions[] = $qb->expr()->isNotNull( + $qb->createFunction( + "JSON_SEARCH(relations, 'one', " . + $qb->createNamedParameter($relation) . + ", NULL, '$')" + ) + ); + } + + // Add the combined OR conditions to query + $qb->andWhere($qb->expr()->orX(...$orConditions)); + + // Remove _relations from filters since it's handled separately + unset($filters['_relations']); + } + + // Filter and search the objects $qb = $this->databaseJsonService->filterJson(builder: $qb, filters: $filters); $qb = $this->databaseJsonService->searchJson(builder: $qb, search: $search); $qb = $this->databaseJsonService->orderJson(builder: $qb, order: $sort); -// var_dump($qb->getSQL()); - return $this->findEntities(query: $qb); } @@ -219,9 +243,12 @@ public function updateFromArray(int $id, array $object): ObjectEntity $obj->hydrate($object); // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if (isset($object['version']) === false) { + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + } + return $this->update($obj); } @@ -265,4 +292,35 @@ public function getFacets(array $filters = [], ?string $search = null) search: $search ); } + + /** + * Find objects that have a specific URI or UUID in their relations + * + * @param string $search The URI or UUID to search for in relations + * @param bool $partialMatch Whether to search for partial matches (default: false) + * @return array An array of ObjectEntities that have the specified URI/UUID + */ + public function findByRelationUri(string $search, bool $partialMatch = false): array + { + $qb = $this->db->getQueryBuilder(); + + // For partial matches, we use '%' wildcards and 'all' mode to search anywhere in the JSON + // For exact matches, we use 'one' mode which finds exact string matches + $mode = $partialMatch ? 'all' : 'one'; + $searchTerm = $partialMatch ? '%' . $search . '%' : $search; + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->isNotNull( + $qb->createFunction( + "JSON_SEARCH(relations, '" . $mode . "', " . + $qb->createNamedParameter($searchTerm) . + ($partialMatch ? ", NULL, '$')" : ")") + ) + ) + ); + + return $this->findEntities($qb); + } } diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index 9dcdf83..569d8f5 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -13,7 +13,7 @@ /** * The RegisterMapper class - * + * * @package OCA\OpenRegister\Db */ class RegisterMapper extends QBMapper @@ -128,9 +128,11 @@ public function updateFromArray(int $id, array $object): Register $obj->hydrate($object); // Update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if (isset($object['version']) === false) { + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + } // Update the register and return it return $this->update($obj); diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 782f75a..571618c 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -79,32 +79,17 @@ public function hydrate(array $object): self */ public function jsonSerialize(): array { + $required = $this->required ?? []; $properties = []; if (isset($this->properties) === true) { - foreach ($this->properties as $key => $property) { - $properties[$key] = $property; - if (isset($property['type']) === false) { - $properties[$key] = $property; - continue; + foreach ($this->properties as $title => $property) { + $title = $property['title'] ?? $title; + if ($property['required'] === true && in_array($title, $required) === false) { + $required[] = $title; } + unset($property['title'], $property['required']); - if (isset($property['format']) === true) { - switch ($property['format']) { - case 'string': - // For now array as string - case 'array': - $properties[$key]['default'] = (string) $property; - break; - case 'int': - case 'integer': - case 'number': - $properties[$key]['default'] = (int) $property; - break; - case 'bool': - $properties[$key]['default'] = (bool) $property; - break; - } - } + $properties[$title] = $property; } } @@ -115,7 +100,7 @@ public function jsonSerialize(): array 'description' => $this->description, 'version' => $this->version, 'summary' => $this->summary, - 'required' => $this->required, + 'required' => $required, 'properties' => $properties, 'archive' => $this->archive, 'source' => $this->source, @@ -143,29 +128,21 @@ public function jsonSerialize(): array public function getSchemaObject(IURLGenerator $urlGenerator): object { $data = $this->jsonSerialize(); - $properties = $data['properties']; - unset($data['properties'], $data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], - $data['updated'], $data['created']); - - $data['required'] = []; - - $data['type'] = 'object'; - - foreach ($properties as $property) { - $title = $property['title']; - if ($property['required'] === true) { - $data['required'][] = $title; - } - unset($property['title'], $property['required']); + foreach ($data['properties'] as $title => $property) { // Remove empty fields with array_filter(). $data['properties'][$title] = array_filter($property); } + unset($data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], + $data['updated'], $data['created']); + + $data['type'] = 'object'; + + // Validator needs this specific $schema $data['$schema'] = 'https://json-schema.org/draft/2020-12/schema'; $data['$id'] = $urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('openregister.Schemas.show', ['id' => $this->getUuid()])); - return json_decode(json_encode($data)); } } diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 2b83986..35b22c3 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -133,9 +133,11 @@ public function updateFromArray(int $id, array $object): Schema $obj->hydrate($object); // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if (isset($object['version']) === false) { + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + } return $this->update($obj); } diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index 3dbfac3..02d0175 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -11,7 +11,7 @@ /** * The SourceMapper class - * + * * @package OCA\OpenRegister\Db */ class SourceMapper extends QBMapper @@ -116,9 +116,11 @@ public function updateFromArray(int $id, array $object): Source $obj->hydrate($object); // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if (isset($object['version']) === false) { + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + } return $this->update($obj); } diff --git a/lib/Migration/Version1Date20241030131427.php b/lib/Migration/Version1Date20241030131427.php index aa8479d..dff2f06 100644 --- a/lib/Migration/Version1Date20241030131427.php +++ b/lib/Migration/Version1Date20241030131427.php @@ -47,6 +47,24 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn(name: 'archive', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); } + if ($table->hasColumn('source') === false) { + $table->addColumn(name: 'source', typeName: Types::STRING, options: ['notnull' => false])->setDefault(default: ''); + } + + // Update the openregister_registers table + $table = $schema->getTable('openregister_registers'); + if ($table->hasColumn('source') === true) { + $column = $table->getColumn('source'); + $column->setNotnull(false); + $column->setDefault(''); + } + + if ($table->hasColumn('table_prefix') === true) { + $column = $table->getColumn('table_prefix'); + $column->setNotnull(false); + $column->setDefault(''); + } + if ($schema->hasTable('openregister_object_audit_logs') === false) { $table = $schema->createTable('openregister_object_audit_logs'); $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]); diff --git a/lib/Service/DownloadService.php b/lib/Service/DownloadService.php index 981b62d..69559fe 100644 --- a/lib/Service/DownloadService.php +++ b/lib/Service/DownloadService.php @@ -2,25 +2,121 @@ namespace OCA\OpenRegister\Service; -use GuzzleHttp\Client; -use GuzzleHttp\Promise\Utils; +use Exception; +use InvalidArgumentException; +use JetBrains\PhpStorm\NoReturn; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; use OCP\IURLGenerator; -use Symfony\Component\Uid\Uuid; +/** + * Service for handling download requests for database entities. + * + * This service enables downloading database entities as files in various formats, + * determined by the `Accept` header of the request. It retrieves the appropriate + * data from mappers and generates responses or downloadable files. + */ class DownloadService { - public function __construct() {} + public function __construct( + private IURLGenerator $urlGenerator, + private SchemaMapper $schemaMapper, + private RegisterMapper $registerMapper + ) {} - public function download(string $type) + /** + * Download a DB entity as a file. Depending on given Accept-header the file type might differ. + * + * @param string $objectType The type of object to download. + * @param string|int $id The id of the object to download. + * @param string $accept The Accept-header from the download request. + * + * @return array The response data for the download request. + * @throws Exception + */ + public function download(string $objectType, string|int $id, string $accept) { - switch ($type) { - case 'json': - // @todo this is placeholder code - break; - default: - // @todo some logging - return null; + // Get the appropriate mapper for the object type + $mapper = $this->getMapper($objectType); + + try { + $object = $mapper->find($id); + } catch (Exception $exception) { + return ['error' => "Could not find $objectType with id $id.", 'statusCode' => 404]; + } + + $objectArray = $object->jsonSerialize(); + $filename = $objectArray['title'].ucfirst($objectType).'-v'.$objectArray['version']; + + if (str_contains(haystack: $accept, needle: 'application/json') === true) { + $url = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.'.ucfirst($objectType).'s.show', ['id' => $object->getId()])); + + $objArray['title'] = $objectArray['title']; + $objArray['$id'] = $url; + $objArray['$schema'] = 'https://docs.commongateway.nl/schemas/'.ucfirst($objectType).'.schema.json'; + $objArray['version'] = $objectArray['version']; + $objArray['type'] = $objectType; + unset($objectArray['title'], $objectArray['version'], $objectArray['id'], $objectArray['uuid']); + $objArray = array_merge($objArray, $objectArray); + + // Convert the object data to JSON + $jsonData = json_encode($objArray, JSON_PRETTY_PRINT); + + $this->downloadJson($jsonData, $filename); } + + return ['error' => "The Accept type $accept is not supported.", 'statusCode' => 400]; + } + + /** + * Generate a downloadable json file response. + * + * @param string $jsonData The json data to create a json file with. + * @param string $filename The filename, .json will be added after this filename in this function. + * + * @return void + */ + #[NoReturn] private function downloadJson(string $jsonData, string $filename): void + { + // Define the file name and path for the temporary JSON file + $fileName = $filename.'.json'; + $filePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName; + + // Create and write the JSON data to the file + file_put_contents($filePath, $jsonData); + + // Set headers to download the file + header('Content-Type: application/json'); + header('Content-Disposition: attachment; filename="' . $fileName . '"'); + header('Content-Length: ' . filesize($filePath)); + + // Output the file contents + readfile($filePath); + + // Clean up: delete the temporary file + unlink($filePath); + exit; // Ensure no further script execution + } + + /** + * Gets the appropriate mapper based on the object type. + * + * @param string $objectType The type of object to retrieve the mapper for. + * + * @return mixed The appropriate mapper. + * @throws InvalidArgumentException If an unknown object type is provided. + * @throws Exception + */ + private function getMapper(string $objectType): mixed + { + $objectTypeLower = strtolower($objectType); + + // If the source is internal, return the appropriate mapper based on the object type + return match ($objectTypeLower) { + 'schema' => $this->schemaMapper, + 'register' => $this->registerMapper, + default => throw new InvalidArgumentException("Unknown object type: $objectType"), + }; } } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index a6f13bf..6356c4b 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -395,17 +395,19 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $oldObject = clone $objectEntity; $objectEntity->setObject($object); - // Ensure UUID exists - if (empty($objectEntity->getUuid())) { + // Ensure UUID exists //@todo: this is not needed anymore? this kinde of uuid is set in the handleLinkRelations function + if (empty($objectEntity->getUuid()) === true) { $objectEntity->setUuid(Uuid::v4()); } - $schemaObject = $this->schemaMapper->find($schema); + // Let grap any links that we can + $objectEntity = $this->handleLinkRelations($objectEntity, $object); + $schemaObject = $this->schemaMapper->find($schema); // Handle object properties that are either nested objects or files - if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties())) { - $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); + if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties()) === true) { + $objectEntity = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); $objectEntity->setObject($object); } @@ -426,6 +428,55 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt return $objectEntity; } + /** + * Handle link relations efficiently using JSON path traversal + * + * Finds all links or UUIDs in the object and adds them to the relations + * using dot notation paths for nested properties + * + * @param ObjectEntity $objectEntity The object entity to handle relations for + * @param array $object The object data + * + * @return ObjectEntity Updated object data + */ + private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity + { + $relations = $objectEntity->getRelations() ?? []; + + // Get object's own identifiers to skip self-references + $selfIdentifiers = [ + $objectEntity->getUri(), + $objectEntity->getUuid(), + $objectEntity->getId() + ]; + + // Function to recursively find links/UUIDs and build dot notation paths + $findRelations = function($data, $path = '') use (&$findRelations, &$relations, $selfIdentifiers) { + foreach ($data as $key => $value) { + $currentPath = $path ? "$path.$key" : $key; + + if (is_array($value) === true) { + // Recurse into nested arrays + $findRelations($value, $currentPath); + } else if (is_string($value) === true) { + // Check for URLs and UUIDs + if ((filter_var($value, FILTER_VALIDATE_URL) !== false + || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1) + && in_array($value, $selfIdentifiers, true) === false + ) { + $relations[$currentPath] = $value; + } + } + } + }; + + // Process the entire object structure + $findRelations($objectEntity->getObject()); + + $objectEntity->setRelations($relations); + return $objectEntity; + } + /** * Handle object relations and file properties in schema properties and array items * @@ -435,13 +486,12 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt * @param int $register The register ID * @param int $schema The schema ID * - * @return array Updated object data + * @return ObjectEntity Updated object with linked data * @throws Exception|ValidationException When file handling fails */ - private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array + private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): ObjectEntity { - - + // @todo: Multidimensional suport should be added foreach ($properties as $propertyName => $property) { // Skip if property not in object if (isset($object[$propertyName]) === false) { @@ -478,13 +528,13 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object // Store relation and replace with reference $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '_' . $index] = $nestedObject->getUuid(); + $relations[$propertyName . '.' . $index] = $nestedObject->getUri(); $objectEntity->setRelations($relations); - $object[$propertyName][$index] = $nestedObject->getUuid(); + $object[$propertyName][$index] = $nestedObject->getUri(); } else { $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '_' . $index] = $item; + $relations[$propertyName . '.' . $index] = $item; $objectEntity->setRelations($relations); } @@ -498,12 +548,17 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } } } + // Handle single object type else if ($property['type'] === 'object') { $subSchema = $schema; - if(is_int($property['$ref']) === true) { + // $ref is a int, id or uuid + if (is_int($property['$ref']) === true + || is_numeric($property['$ref']) === true + || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $property['$ref']) === 1 + ) { $subSchema = $property['$ref']; } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { $parsedUrl = parse_url($property['$ref']); @@ -520,9 +575,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object // Store relation and replace with reference $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $nestedObject->getUuid(); + $relations[$propertyName] = $nestedObject->getUri(); $objectEntity->setRelations($relations); - $object[$propertyName] = $nestedObject->getUuid(); + $object[$propertyName] = $nestedObject->getUri(); } else { $relations = $objectEntity->getRelations() ?? []; @@ -542,7 +597,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } } - return $object; + $objectEntity->setObject($object); + + return $objectEntity; } /** diff --git a/lib/Service/UploadService.php b/lib/Service/UploadService.php index f14ae18..7699788 100644 --- a/lib/Service/UploadService.php +++ b/lib/Service/UploadService.php @@ -12,6 +12,14 @@ use Symfony\Component\Uid\Uuid; use Symfony\Component\Yaml\Yaml; +/** + * Service for handling file and JSON uploads. + * + * This service processes uploaded JSON data, either directly via a POST body, + * from a provided URL, or from an uploaded file. It supports multiple data + * formats (e.g., JSON, YAML) and integrates with schemas and registers for + * database updates. + */ class UploadService { public function __construct( @@ -56,7 +64,9 @@ public function getUploadedJson(array $data): array|JSONResponse } if (empty($data['url']) === false) { - return $this->getJSONfromURL($data['url']); + $phpArray = $this->getJSONfromURL($data['url']); + $phpArray['source'] = $data['url']; + return $phpArray; } $phpArray = $data['json']; diff --git a/src/entities/object/object.mock.ts b/src/entities/object/object.mock.ts index fb6e45f..0eaab47 100644 --- a/src/entities/object/object.mock.ts +++ b/src/entities/object/object.mock.ts @@ -5,18 +5,24 @@ export const mockObjectData = (): TObject[] => [ { id: '1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', uuid: 'uuid-1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + uri: 'https://example.com/character/1234a1e5-b54d-43ad-abd1-4b5bff5fcd3f', register: 'Character Register', schema: 'character_schema', object: JSON.stringify({ key: 'value' }), + relations: JSON.stringify({ key: 'value' }), + files: 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', + uri: 'https://example.com/item/5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', register: 'Item Register', schema: 'item_schema', object: JSON.stringify({ key: 'value' }), + relations: JSON.stringify({ key: 'value' }), + files: JSON.stringify({ key: 'value' }), created: new Date().toISOString(), updated: new Date().toISOString(), }, diff --git a/src/entities/object/object.spec.ts b/src/entities/object/object.spec.ts index d9232f9..f66583e 100644 --- a/src/entities/object/object.spec.ts +++ b/src/entities/object/object.spec.ts @@ -17,9 +17,12 @@ describe('Object Entity', () => { expect(object).toBeInstanceOf(Object) expect(object.id).toBe('') expect(object.uuid).toBe(mockObjectData()[0].uuid) + expect(object.uri).toBe(mockObjectData()[0].uri) expect(object.register).toBe(mockObjectData()[0].register) expect(object.schema).toBe(mockObjectData()[0].schema) expect(object.object).toBe(mockObjectData()[0].object) + expect(object.relations).toBe(mockObjectData()[0].relations) + expect(object.files).toBe(mockObjectData()[0].files) expect(object.updated).toBe(mockObjectData()[0].updated) expect(object.created).toBe(mockObjectData()[0].created) expect(object.validate().success).toBe(true) diff --git a/src/entities/object/object.ts b/src/entities/object/object.ts index a78523e..cec2b84 100644 --- a/src/entities/object/object.ts +++ b/src/entities/object/object.ts @@ -5,18 +5,24 @@ export class ObjectEntity implements TObject { public id: string public uuid: string + public uri: string public register: string public schema: string public object: string + public relations: string + public files: string public updated: string public created: string constructor(object: TObject) { this.id = object.id || '' this.uuid = object.uuid + this.uri = object.uri this.register = object.register this.schema = object.schema this.object = object.object + this.relations = object.relations + this.files = object.files this.updated = object.updated || '' this.created = object.created || '' } @@ -28,6 +34,8 @@ export class ObjectEntity implements TObject { register: z.string().min(1), schema: z.string().min(1), object: z.string(), + relations: z.string(), + files: z.string(), updated: z.string(), created: z.string(), }) diff --git a/src/entities/object/object.types.ts b/src/entities/object/object.types.ts index 4aa6ea2..b37dc3c 100644 --- a/src/entities/object/object.types.ts +++ b/src/entities/object/object.types.ts @@ -1,9 +1,12 @@ export type TObject = { id?: string uuid: string + uri: string register: string schema: string object: string // JSON object + relations: string + files: string updated: string created: string } diff --git a/src/modals/schema/EditSchemaProperty.vue b/src/modals/schema/EditSchemaProperty.vue index 1773de8..b46abb5 100644 --- a/src/modals/schema/EditSchemaProperty.vue +++ b/src/modals/schema/EditSchemaProperty.vue @@ -38,10 +38,59 @@ import { navigationStore, schemaStore } from '../../store/store.js' v-model="properties.format" :disabled="properties.type !== 'string'" /> + +
+
+ Object Configuration: +
+ + +
- + +
+ + + + +
+ +
@@ -151,10 +200,6 @@ import { navigationStore, schemaStore } from '../../store/store.js' label="Default value" :value.sync="properties.default" /> - - @@ -167,16 +212,6 @@ import { navigationStore, schemaStore } from '../../store/store.js' Deprecated - - - - @@ -229,7 +264,7 @@ import { navigationStore, schemaStore } from '../../store/store.js'
@@ -331,6 +366,16 @@ export default { $ref: '', type: '', }, + objectConfiguration: { + handling: 'nested-object', + schema: '', + }, + fileConfiguration: { + handling: 'ignore', + allowedMimeTypes: [], + location: '', // Initialize with empty string + maxSize: 0, // Initialize with 0 + }, }, typeOptions: { inputLabel: 'Type*', @@ -347,6 +392,30 @@ export default { multiple: false, options: ['date', 'time', 'duration', 'date-time', 'url', 'uri', 'uuid', 'email', 'idn-email', 'hostname', 'idn-hostname', 'ipv4', 'ipv6', 'uri-reference', 'iri', 'iri-reference', 'uri-template', 'json-pointer', 'regex', 'binary', 'byte', 'password', 'rsin', 'kvk', 'bsn', 'oidn', 'telephone'], }, + objectConfiguration: { + handling: { + inputLabel: 'Object Handeling', + multiple: false, + options: ['nested-object', 'nested-schema', 'related-schema', 'uri'], + }, + }, + fileConfiguration: { + handling: { + inputLabel: 'File Configuration', + multiple: false, + options: ['ignore', 'transform'], + }, + }, + availableSchemas: { + inputLabel: 'Select Schema', + multiple: false, + options: ['schema1', 'schema2', 'schema3'], // This should be populated with actual schemas + }, + mimeTypes: { + inputLabel: 'Allowed MIME Types', + multiple: true, + options: ['image/jpeg', 'image/png', 'application/pdf', 'text/plain'], // Add more MIME types as needed + }, loading: false, success: null, error: false, @@ -391,7 +460,8 @@ export default { this.propertyTitle = schemaStore.schemaPropertyKey this.properties = { - ...schemaProperty, + ...this.properties, // Preserve default structure + ...schemaProperty, // Override with existing values minLength: schemaProperty.minLength ?? 0, maxLength: schemaProperty.maxLength ?? 0, minimum: schemaProperty.minimum ?? 0, @@ -399,6 +469,15 @@ export default { multipleOf: schemaProperty.multipleOf ?? 0, minItems: schemaProperty.minItems ?? 0, maxItems: schemaProperty.maxItems ?? 0, + // Preserve nested configurations with existing values or defaults + objectConfiguration: { + ...this.properties.objectConfiguration, + ...(schemaProperty.objectConfiguration || {}), + }, + fileConfiguration: { + ...this.properties.fileConfiguration, + ...(schemaProperty.fileConfiguration || {}), + }, } } }, @@ -444,7 +523,7 @@ export default { }, } - if (!newSchemaItem.properties[this.propertyTitle].items.$ref && !newSchemaItem[this.propertyTitle].items.type) { + if (!newSchemaItem.properties[this.propertyTitle].items.$ref && !newSchemaItem.properties[this.propertyTitle].items.type) { delete newSchemaItem.properties[this.propertyTitle].items } @@ -482,7 +561,7 @@ export default { diff --git a/src/store/modules/object.js b/src/store/modules/object.js index 6098879..b8befb9 100644 --- a/src/store/modules/object.js +++ b/src/store/modules/object.js @@ -8,6 +8,8 @@ export const useObjectStore = defineStore('object', { objectList: [], auditTrailItem: false, auditTrails: [], + relationItem: false, + relations: [], }), actions: { setObjectItem(objectItem) { @@ -28,6 +30,14 @@ export const useObjectStore = defineStore('object', { (auditTrail) => new AuditTrail(auditTrail), ) }, + setRelationItem(relationItem) { + this.relationItem = relationItem && new ObjectEntity(relationItem) + }, + setRelations(relations) { + this.relations = relations.map( + (relation) => new ObjectEntity(relation), + ) + }, /* istanbul ignore next */ // ignore this for Jest until moved into a service async refreshObjectList(search = null) { // @todo this might belong in a service? @@ -63,6 +73,7 @@ export const useObjectStore = defineStore('object', { const data = await response.json() this.setObjectItem(data) this.getAuditTrails(data.id) + this.getRelations(data.id) return data } catch (err) { @@ -141,6 +152,7 @@ export const useObjectStore = defineStore('object', { this.refreshObjectList() this.setObjectItem(data) this.getAuditTrails(data.id) + this.getRelations(data.id) return { response, data } }, @@ -163,6 +175,25 @@ export const useObjectStore = defineStore('object', { return { response, data } }, + // RELATIONS + async getRelations(id) { + if (!id) { + throw new Error('No object id to get relations for') + } + + const endpoint = `/index.php/apps/openregister/api/objects/relations/${id}` + + const response = await fetch(endpoint, { + method: 'GET', + }) + + const responseData = await response.json() + const data = responseData.map((relation) => new ObjectEntity(relation)) + + this.setRelations(data) + + return { response, data } + }, // mappings async getMappings() { const endpoint = '/index.php/apps/openregister/api/objects/mappings' diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue index c072b79..4160618 100644 --- a/src/views/object/ObjectDetails.vue +++ b/src/views/object/ObjectDetails.vue @@ -29,7 +29,7 @@ import { objectStore, navigationStore } from '../../store/store.js'
- {{ objectStore.objectItem.uuid }} + Uri: {{ objectStore.objectItem.uri }}
Register: @@ -56,6 +56,51 @@ import { objectStore, navigationStore } from '../../store/store.js' -->{{ JSON.stringify(objectStore.objectItem.object, null, 2) }} + +
+ + + + +
+
+ No relations found +
+
+ +
+ + + + +
+
+ No relations found +
+
+ +
+ {{ JSON.stringify(objectStore.objectItem.files, null, 2) }} +
+
No synchronizations found @@ -111,6 +156,7 @@ import Pencil from 'vue-material-design-icons/Pencil.vue' import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' import TimelineQuestionOutline from 'vue-material-design-icons/TimelineQuestionOutline.vue' import Eye from 'vue-material-design-icons/Eye.vue' +import CubeOutline from 'vue-material-design-icons/CubeOutline.vue' export default { name: 'ObjectDetails', @@ -124,24 +170,30 @@ export default { Pencil, TrashCanOutline, TimelineQuestionOutline, + CubeOutline, + Eye, }, data() { return { currentActiveObject: undefined, auditTrailLoading: false, auditTrails: [], + relationsLoading: false, + relations: [], } }, mounted() { if (objectStore.objectItem?.id) { this.currentActiveObject = objectStore.objectItem?.id this.getAuditTrails() + this.getRelations() } }, updated() { if (this.currentActiveObject !== objectStore.objectItem?.id) { this.currentActiveObject = objectStore.objectItem?.id this.getAuditTrails() + this.getRelations() } }, methods: { @@ -154,6 +206,15 @@ export default { this.auditTrailLoading = false }) }, + getRelations() { + this.relationsLoading = true + + objectStore.getRelations(objectStore.objectItem.id) + .then(({ data }) => { + this.relations = data + this.relationsLoading = false + }) + }, }, } diff --git a/src/views/object/ObjectsList.vue b/src/views/object/ObjectsList.vue index df3d706..e1221d3 100644 --- a/src/views/object/ObjectsList.vue +++ b/src/views/object/ObjectsList.vue @@ -44,7 +44,7 @@ import { objectStore, navigationStore, searchStore } from '../../store/store.js' :force-display-actions="true" @click="objectStore.setObjectItem(object)"> @@ -84,7 +84,7 @@ import { objectStore, navigationStore, searchStore } from '../../store/store.js'