From 87216f3fb98827fa23009d406b2e05a6f22547cd Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Tue, 10 Dec 2024 16:42:20 +0100 Subject: [PATCH 01/21] Reinstate validation, add resolve for internal schema entities --- lib/Db/Schema.php | 2 +- lib/Service/ObjectService.php | 38 +++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 571618c..5c40c43 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -141,7 +141,7 @@ public function getSchemaObject(IURLGenerator $urlGenerator): 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()])); + $data['$id'] = $urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('openregister.Schemas.show', ['id' => $this->getId()])); return json_decode(json_encode($data)); } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 6356c4b..1d1cae6 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -23,6 +23,7 @@ use OCP\IURLGenerator; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; +use Opis\Uri\Uri; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -100,6 +101,29 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m return null; } + /** + * Fetch schema from URL + * + * @param Uri $uri The URI registered by the resolver. + * + * @return string The resulting json object. + */ + public function fetchSchema(Uri $uri):string + { + if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() + && str_contains(haystack: $uri->path(), needle: '/api/schemas') === true + ) { + $exploded = explode(separator: '/', string: $uri->path()); + $schema = $this->schemaMapper->find(end($exploded)); + + return json_encode($schema->getSchemaObject($this->urlGenerator)); + } + + // @TODO: Decide if we also want to accept truly external schemas, and if so, implement them. + + return ''; + } + /** * Validate an object with a schema. * If schema is not given and schemaObject is filled, the object will validate to the schemaObject. @@ -119,6 +143,8 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); + $validator->loader()->resolver()->registerProtocol('http', [$this, 'fetchSchema']); + return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); @@ -374,7 +400,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt ); } -// $validationResult = $this->validateObject(object: $object, schemaId: $schema); + $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists if (isset($object['id']) === false || $objectEntity === null) { @@ -413,17 +439,17 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $objectEntity->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()]))); - if ($objectEntity->getId()) {// && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ + if ($objectEntity->getId() && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ $objectEntity = $this->objectEntityMapper->update($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); - } else {//if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { + } else if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { $objectEntity = $this->objectEntityMapper->insert($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity); } -// if ($validationResult->isValid() === false) { -// throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); -// } + if ($validationResult->isValid() === false) { + throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); + } return $objectEntity; } From 80889795890e6f79a5697f4d1c9f9078dd64c858 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 11 Dec 2024 16:48:46 +0100 Subject: [PATCH 02/21] validation of files, oneof etc. first draft --- lib/Db/File.php | 130 +++++++++++++ lib/Db/FileMapper.php | 104 ++++++++++ lib/Db/Schema.php | 17 ++ lib/Service/ObjectService.php | 352 ++++++++++++++++++++++++---------- 4 files changed, 498 insertions(+), 105 deletions(-) create mode 100644 lib/Db/File.php create mode 100644 lib/Db/FileMapper.php diff --git a/lib/Db/File.php b/lib/Db/File.php new file mode 100644 index 0000000..0c3f57d --- /dev/null +++ b/lib/Db/File.php @@ -0,0 +1,130 @@ +addType('uuid', 'string'); + $this->addType('filename', 'string'); + $this->addType('downloadUrl', 'string'); + $this->addType('shareUrl', 'string'); + $this->addType('accessUrl', 'string'); + $this->addType('extension', 'string'); + $this->addType('checksum', 'string'); + $this->addType('source', 'int'); + $this->addType('userId', 'string'); + $this->addType('created', 'datetime'); + $this->addType('updated', '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(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { +// ("Error writing $key"); + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'filename' => $this->filename, + 'downloadUrl' => $this->downloadUrl, + 'shareUrl' => $this->shareUrl, + 'accessUrl' => $this->accessUrl, + 'extension' => $this->extension, + 'checksum' => $this->checksum, + 'source' => $this->source, + 'userId' => $this->userId, + 'created' => isset($this->created) ? $this->created->format('c') : null, + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + ]; + } + + public static function getSchema(IURLGenerator $IURLGenerator): string { + return json_encode([ + '$id' => $IURLGenerator->getBaseUrl().'/apps/openconnector/api/files/schema', + '$schema' => 'https://json-schema.org/draft/2020-12/schema', + 'type' => 'object', + 'required' => [ + 'filename', + 'accessUrl', + 'userId', + ], + 'properties' => [ + 'filename' => [ + 'type' => 'string', + 'minLength' => 1, + 'maxLength' => 255 + ], + 'downloadUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'shareUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'accessUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'extension' => [ + 'type' => 'string', + 'maxLength' => 10, + ], + 'checksum' => [ + 'type' => 'string', + ], + 'source' => [ + 'type' => 'number', + ], + 'userId' => [ + 'type' => 'string', + ] + ] + ]); + } +} diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php new file mode 100644 index 0000000..e3e29ff --- /dev/null +++ b/lib/Db/FileMapper.php @@ -0,0 +1,104 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_jobs') + ->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('openconnector_jobs') + ->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) === false) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + + return $this->findEntities(query: $qb); + } + + public function createFromArray(array $object): File + { + $obj = new File(); + $obj->hydrate($object); + // Set uuid + if ($obj->getUuid() === null){ + $obj->setUuid(Uuid::v4()); + } + return $this->insert(entity: $obj); + } + + public function updateFromArray(int $id, array $object): File + { + $obj = $this->find($id); + $obj->hydrate($object); + + // Set or update the version + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + + return $this->update($obj); + } + + /** + * Get the total count of all call logs. + * + * @return int The total number of call logs in the database. + */ + public function getTotalCallCount(): int + { + $qb = $this->db->getQueryBuilder(); + + // Select count of all logs + $qb->select($qb->createFunction('COUNT(*) as count')) + ->from('openconnector_jobs'); + + $result = $qb->execute(); + $row = $result->fetch(); + + // Return the total count + return (int)$row['count']; + } +} diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 5c40c43..15c5267 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -132,6 +132,23 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object foreach ($data['properties'] as $title => $property) { // Remove empty fields with array_filter(). $data['properties'][$title] = array_filter($property); + + if($property['type'] === 'file') { + $data['properties'][$title] = ['$ref' => $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema']; + } + if($property['type'] === 'oneOf') { + unset($data['properties'][$title]['type']); + $data['properties'][$title]['oneOf'] = array_map( + callback: function (array $item) use ($urlGenerator) { + if($item['type'] === 'file') { + unset($item['type']); + $item['$ref'] = $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema'; + } + + return $item; + }, + array: $property['oneOf']); + } } unset($data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 1d1cae6..0dd2075 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -7,6 +7,7 @@ use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use OC\URLGenerator; +use OCA\OpenRegister\Db\File; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; @@ -20,7 +21,9 @@ use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Formats\BsnFormat; use OCP\App\IAppManager; +use OCP\IAppConfig; use OCP\IURLGenerator; +use Opis\JsonSchema\Errors\ErrorFormatter; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; use Opis\Uri\Uri; @@ -71,7 +74,8 @@ public function __construct( private ContainerInterface $container, private readonly IURLGenerator $urlGenerator, private readonly FileService $fileService, - private readonly IAppManager $appManager + private readonly IAppManager $appManager, + private readonly IAppConfig $config, ) { $this->objectEntityMapper = $objectEntityMapper; @@ -105,10 +109,12 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m * Fetch schema from URL * * @param Uri $uri The URI registered by the resolver. - * + * * @return string The resulting json object. + * + * @throws GuzzleException */ - public function fetchSchema(Uri $uri):string + public function resolveSchema(Uri $uri): string { if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() && str_contains(haystack: $uri->path(), needle: '/api/schemas') === true @@ -119,7 +125,21 @@ public function fetchSchema(Uri $uri):string return json_encode($schema->getSchemaObject($this->urlGenerator)); } - // @TODO: Decide if we also want to accept truly external schemas, and if so, implement them. + if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() + && str_contains(haystack: $uri->path(), needle: '/api/files/schema') === true + ) { + $exploded = explode(separator: '/', string: $uri->path()); + return File::getSchema($this->urlGenerator); + } + + // @TODO: Validate file schema + + if ($this->config->getValueBool(app: 'openregister', key: 'allowExternalSchemas') === true) { + $client = new Client(); + $result = $client->get(\GuzzleHttp\Psr7\Uri::fromParts($uri->components())); + + return $result->getBody()->getContents(); + } return ''; } @@ -140,10 +160,14 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $schemaObject = $this->schemaMapper->find($schemaId)->getSchemaObject($this->urlGenerator); } + if($schemaObject->properties === []) { + $schemaObject->properties = new stdClass(); + } + $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); - $validator->loader()->resolver()->registerProtocol('http', [$this, 'fetchSchema']); + $validator->loader()->resolver()->registerProtocol('http', [$this, 'resolveSchema']); return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); @@ -503,6 +527,216 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity return $objectEntity; } + private function addObject + ( + array $property, + string $propertyName, + array $item, + ObjectEntity $objectEntity, + int $register, + int $schema, + ?int $index = null, + ): string + { + $subSchema = $schema; + if(is_int($property['$ref']) === true) { + $subSchema = $property['$ref']; + } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property['$ref']); + $explodedPath = explode(separator: '/', string: $parsedUrl['path']); + $subSchema = end($explodedPath); + } + + // Handle nested object in array + $nestedObject = $this->saveObject( + register: $register, + schema: $subSchema, + object: $item + ); + + if($index === null) { + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $nestedObject->getUri(); + $objectEntity->setRelations($relations); + } else { + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName . '.' . $index] = $nestedObject->getUri(); + $objectEntity->setRelations($relations); + } + + return $nestedObject->getUri(); + } + + private function handleObjectProperty( + array $property, + string $propertyName, + array $item, + ObjectEntity $objectEntity, + int $register, + int $schema + ): string + { + return $this->addObject( + property: $property,propertyName: $propertyName, item: $item, objectEntity: $objectEntity, register: $register, schema: $schema + ); + } + + private function handleArrayProperty( + array $property, + string $propertyName, + array $items, + ObjectEntity $objectEntity, + int $register, + int $schema + ): array + { + if(isset($property['items']) === false) { + return $items; + } + + if(isset($property['items']['oneOf'])) { + foreach($items as $index=>$item) { + $items[$index] = $this->handleOneOfProperty( + property: $property['items'], + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema + ); + } + return $items; + } + + if ($property['items']['type'] !== 'object' + && $property['items']['type'] !== 'file' + ) { + return $items; + } + + if ($property['items']['type'] === 'file') + { + foreach($items as $index => $item) { + $items[$index] = $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => [$index => $item]], + propertyName: $propertyName . '.' . $index + )[$propertyName]; + } + return $items; + } + + foreach($items as $index=>$item) { + $items[$index] = $this->addObject( + property: $property['items'], + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema, + index: $index + ); + } + + return $items; + } + + private function handleOneOfProperty( + array $property, + string $propertyName, + string|array $item, + ObjectEntity $objectEntity, + int $register, + int $schema, + ?int $index = null + ): string|array + { + if (array_is_list($property) === false) { + return $item; + } + + if (array_column(array: $property, column_key: '$ref') === []) { + return $item; + } + + if (is_array($item) === false) { + return $item; + } + + $oneOf = array_filter( + array: $property, + callback: function (array $option) { + return isset($option['$ref']); + } + )[0]; + + return $this->addObject( + property: $oneOf, + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema, + index: $index + ); + } + + private function handleProperty ( + array $property, + string $propertyName, + int $register, + int $schema, + array $object, + ObjectEntity $objectEntity + ): ObjectEntity + { + switch($property['type']) { + case 'object': + $object[$propertyName] = $this->handleObjectProperty( + property: $property, + propertyName: $propertyName, + item: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema, + ); + break; + case 'array': + $object[$propertyName] = $this->handleArrayProperty( + property: $property, + propertyName: $propertyName, + items: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema, + ); + break; + case 'oneOf': + $object[$propertyName] = $this->handleOneOfProperty( + property: $property, + propertyName: $propertyName, + item: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema,); + break; + case 'file': + $object[$propertyName] = $this->handleFileProperty( + objectEntity: $objectEntity, + object: $object, + propertyName: $propertyName + ); + default: + break; + } + + $objectEntity->setObject($object); + + return $objectEntity; + } + + /** * Handle object relations and file properties in schema properties and array items * @@ -524,107 +758,15 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object continue; } - // Handle array type with items that may contain objects/files - if ($property['type'] === 'array' && isset($property['items']) === true) { - // Skip if not array in data - if (is_array($object[$propertyName]) === false) { - continue; - } - - // Process each array item - foreach ($object[$propertyName] as $index => $item) { - if ($property['items']['type'] === 'object') { - $subSchema = $schema; - - if(is_int($property['items']['$ref']) === true) { - $subSchema = $property['items']['$ref']; - } else if (filter_var(value: $property['items']['$ref'], filter: FILTER_VALIDATE_URL) !== false) { - $parsedUrl = parse_url($property['items']['$ref']); - $explodedPath = explode(separator: '/', string: $parsedUrl['path']); - $subSchema = end($explodedPath); - } - - if(is_array($item) === true) { - // Handle nested object in array - $nestedObject = $this->saveObject( - register: $register, - schema: $subSchema, - object: $item - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '.' . $index] = $nestedObject->getUri(); - $objectEntity->setRelations($relations); - $object[$propertyName][$index] = $nestedObject->getUri(); - - } else { - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '.' . $index] = $item; - $objectEntity->setRelations($relations); - } - - } else if ($property['items']['type'] === 'file') { - // Handle file in array - $object[$propertyName][$index] = $this->handleFileProperty( - objectEntity: $objectEntity, - object: [$propertyName => [$index => $item]], - propertyName: $propertyName . '.' . $index - )[$propertyName]; - } - } - } - - // Handle single object type - else if ($property['type'] === 'object') { - - $subSchema = $schema; - - // $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']); - $explodedPath = explode(separator: '/', string: $parsedUrl['path']); - $subSchema = end($explodedPath); - } - - if(is_array($object[$propertyName]) === true) { - $nestedObject = $this->saveObject( - register: $register, - schema: $subSchema, - object: $object[$propertyName] - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $nestedObject->getUri(); - $objectEntity->setRelations($relations); - $object[$propertyName] = $nestedObject->getUri(); - - } else { - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $object[$propertyName]; - $objectEntity->setRelations($relations); - } - - } - // Handle single file type - else if ($property['type'] === 'file') { - - $object = $this->handleFileProperty( - objectEntity: $objectEntity, - object: $object, - propertyName: $propertyName - ); - } + $objectEntity = $this->handleProperty( + property: $property, + propertyName: $propertyName, + register: $register, + schema: $schema, + object: $object, + objectEntity: $objectEntity, + ); } - - $objectEntity->setObject($object); - return $objectEntity; } From 03a4bb7cf5405fd2c9a224623ac6c1ef413fbc4e Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 12:51:07 +0100 Subject: [PATCH 03/21] Major cleanup on subobjects --- lib/Db/File.php | 1 - lib/Db/Schema.php | 5 +++ lib/Service/ObjectService.php | 60 ++++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index 0c3f57d..18fdb7c 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -91,7 +91,6 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { 'required' => [ 'filename', 'accessUrl', - 'userId', ], 'properties' => [ 'filename' => [ diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 15c5267..937d8e4 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -149,6 +149,11 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object }, array: $property['oneOf']); } + if($property['type'] === 'array' + && isset($property['items']['type']) === true + && $property['items']['type'] === 'oneOf') { + unset($data['properties'][$title]['items']['type']); + } } unset($data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 0dd2075..cbdf149 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -458,7 +458,6 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt // Handle object properties that are either nested objects or files if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties()) === true) { $objectEntity = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); - $objectEntity->setObject($object); } $objectEntity->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()]))); @@ -565,7 +564,7 @@ private function addObject $objectEntity->setRelations($relations); } - return $nestedObject->getUri(); + return $nestedObject->getUuid(); } private function handleObjectProperty( @@ -598,12 +597,13 @@ private function handleArrayProperty( if(isset($property['items']['oneOf'])) { foreach($items as $index=>$item) { $items[$index] = $this->handleOneOfProperty( - property: $property['items'], + property: $property['items']['oneOf'], propertyName: $propertyName, item: $item, objectEntity: $objectEntity, register: $register, - schema: $schema + schema: $schema, + index: $index ); } return $items; @@ -656,6 +656,27 @@ private function handleOneOfProperty( return $item; } + if (in_array(needle:'file', haystack: array_column(array: $property, column_key: 'type')) === true + && is_array($item) === true + && $index !== null + ) { + return $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => [$index => $item]], + propertyName: $propertyName + ); + } + if (in_array(needle:'file', haystack: array_column(array: $property, column_key: 'type')) === true + && is_array($item) === true + && $index === null + ) { + return $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => $item], + propertyName: $propertyName + ); + } + if (array_column(array: $property, column_key: '$ref') === []) { return $item; } @@ -667,7 +688,7 @@ private function handleOneOfProperty( $oneOf = array_filter( array: $property, callback: function (array $option) { - return isset($option['$ref']); + return isset($option['$ref']) === true; } )[0]; @@ -689,7 +710,7 @@ private function handleProperty ( int $schema, array $object, ObjectEntity $objectEntity - ): ObjectEntity + ): array { switch($property['type']) { case 'object': @@ -714,12 +735,12 @@ private function handleProperty ( break; case 'oneOf': $object[$propertyName] = $this->handleOneOfProperty( - property: $property, + property: $property['oneOf'], propertyName: $propertyName, item: $object[$propertyName], objectEntity: $objectEntity, register: $register, - schema: $schema,); + schema: $schema); break; case 'file': $object[$propertyName] = $this->handleFileProperty( @@ -731,9 +752,7 @@ private function handleProperty ( break; } - $objectEntity->setObject($object); - - return $objectEntity; + return $object; } @@ -758,7 +777,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object continue; } - $objectEntity = $this->handleProperty( + $object = $this->handleProperty( property: $property, propertyName: $propertyName, register: $register, @@ -767,6 +786,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object objectEntity: $objectEntity, ); } + + $objectEntity->setObject($object); + return $objectEntity; } @@ -785,18 +807,6 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $fileName = str_replace('.', '_', $propertyName); $objectDot = new Dot($object); - // Check if it's a Nextcloud file URL -// if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { -// $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); -// if (preg_match('/\/f\/(\d+)/', $urlPath, $matches)) { -// $files = $objectEntity->getFiles() ?? []; -// $files[$propertyName] = (int)$matches[1]; -// $objectEntity->setFiles($files); -// $object[$propertyName] = (int)$matches[1]; -// return $object; -// } -// } - // Handle base64 encoded file if (is_string($objectDot->get($propertyName)) === true && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches) @@ -809,7 +819,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s // Handle URL file else { // Encode special characters in the URL - $encodedUrl = rawurlencode($objectDot->get("$propertyName.downloadUrl")); //@todo hardcoded .downloadUrl + $encodedUrl = rawurlencode($objectDot->get("$propertyName.accessUrl")); //@todo hardcoded .downloadUrl // Decode valid path separators and reserved characters $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); From 6f06d31d5df03b0a332be32ed6e4fe6652d9cf78 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 13:49:48 +0100 Subject: [PATCH 04/21] Add docblocks to new functions --- lib/Service/ObjectService.php | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index cbdf149..86b883d 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -526,6 +526,20 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity return $objectEntity; } + /** + * Adds a subobject based upon given parameters and adds it to the main object. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * @param int|null $index If the subobject is in an array, the index of the object in the array. + * + * @return string The updated item + * @throws ValidationException + */ private function addObject ( array $property, @@ -567,6 +581,18 @@ private function addObject return $nestedObject->getUuid(); } + /** + * Handles a property that is of the type array. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * + * @return string The updated item + */ private function handleObjectProperty( array $property, string $propertyName, @@ -581,6 +607,19 @@ private function handleObjectProperty( ); } + /** + * Handles a property that is of the type array. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $items The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * + * @return array The updated item + * @throws GuzzleException + */ private function handleArrayProperty( array $property, string $propertyName, @@ -642,6 +681,20 @@ private function handleArrayProperty( return $items; } + /** + * Handles a property that of the type oneOf. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param string|array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * @param int|null $index If the oneOf is in an array, the index within the array + * + * @return string|array The updated item + * @throws GuzzleException + */ private function handleOneOfProperty( array $property, string $propertyName, @@ -703,6 +756,20 @@ private function handleOneOfProperty( ); } + /** + * Rewrites subobjects stored in separate objectentities to the Uuid of that object, + * rewrites files to the chosen format + * + * @param array $property The content of the property in the schema + * @param string $propertyName The name of the property + * @param int $register The register the main object is in + * @param int $schema The schema of the main object + * @param array $object The object to rewrite + * @param ObjectEntity $objectEntity The objectEntity to write the object in + * + * @return array The resulting object + * @throws GuzzleException + */ private function handleProperty ( array $property, string $propertyName, From 3e2a0a1f7e12ce4d7b32c2df4d1f807c7b070df2 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 12 Dec 2024 14:06:16 +0100 Subject: [PATCH 05/21] Code style fixes --- lib/Db/FileMapper.php | 4 ++-- lib/Db/ObjectAuditLogMapper.php | 2 +- lib/Db/ObjectEntityMapper.php | 2 +- lib/Db/RegisterMapper.php | 2 +- lib/Db/Schema.php | 8 ++++---- lib/Db/SchemaMapper.php | 2 +- lib/Db/SourceMapper.php | 2 +- lib/Service/FileService.php | 2 +- lib/Service/MySQLJsonService.php | 2 +- lib/Service/ObjectService.php | 20 +++++++++----------- 10 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index e3e29ff..bd7f2b3 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -76,7 +76,7 @@ public function updateFromArray(int $id, array $object): File // Set or update the version $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); return $this->update($obj); @@ -99,6 +99,6 @@ public function getTotalCallCount(): int $row = $result->fetch(); // Return the total count - return (int)$row['count']; + return (int) $row['count']; } } diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php index f31fdaf..387fb6f 100644 --- a/lib/Db/ObjectAuditLogMapper.php +++ b/lib/Db/ObjectAuditLogMapper.php @@ -78,7 +78,7 @@ public function updateFromArray(int $id, array $object): ObjectAuditLog // Set or update the version if (isset($object['version']) === false) { $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); } diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 860523d..bf5d760 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -245,7 +245,7 @@ public function updateFromArray(int $id, array $object): ObjectEntity // Set or update the version if (isset($object['version']) === false) { $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); } diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index 569d8f5..1f573d8 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -130,7 +130,7 @@ public function updateFromArray(int $id, array $object): Register // Update the version if (isset($object['version']) === false) { $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); } diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 937d8e4..db08bf7 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -133,14 +133,14 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object // Remove empty fields with array_filter(). $data['properties'][$title] = array_filter($property); - if($property['type'] === 'file') { + if ($property['type'] === 'file') { $data['properties'][$title] = ['$ref' => $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema']; } - if($property['type'] === 'oneOf') { + if ($property['type'] === 'oneOf') { unset($data['properties'][$title]['type']); $data['properties'][$title]['oneOf'] = array_map( callback: function (array $item) use ($urlGenerator) { - if($item['type'] === 'file') { + if ($item['type'] === 'file') { unset($item['type']); $item['$ref'] = $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema'; } @@ -149,7 +149,7 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object }, array: $property['oneOf']); } - if($property['type'] === 'array' + if ($property['type'] === 'array' && isset($property['items']['type']) === true && $property['items']['type'] === 'oneOf') { unset($data['properties'][$title]['items']['type']); diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 35b22c3..11f3953 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -135,7 +135,7 @@ public function updateFromArray(int $id, array $object): Schema // Set or update the version if (isset($object['version']) === false) { $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); } diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index 02d0175..7151106 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -118,7 +118,7 @@ public function updateFromArray(int $id, array $object): Source // Set or update the version if (isset($object['version']) === false) { $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; + $version[2] = (int) $version[2] + 1; $obj->setVersion(implode('.', $version)); } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 9d547b5..2d9a832 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -97,7 +97,7 @@ private function getCurrentDomain(): string $baseUrl = $this->urlGenerator->getBaseUrl(); $trustedDomains = $this->config->getSystemValue('trusted_domains'); - if(isset($trustedDomains[1]) === true) { + if (isset($trustedDomains[1]) === true) { $baseUrl = str_replace(search: 'localhost', replace: $trustedDomains[1], subject: $baseUrl); } diff --git a/lib/Service/MySQLJsonService.php b/lib/Service/MySQLJsonService.php index 3331ef1..4307420 100644 --- a/lib/Service/MySQLJsonService.php +++ b/lib/Service/MySQLJsonService.php @@ -140,7 +140,7 @@ public function filterJson(IQueryBuilder $builder, array $filters): IQueryBuilde // Create parameter for JSON path $builder->createNamedParameter(value: "$.$filter", placeHolder: ":path$filter"); - if(is_array($value) === true && array_is_list($value) === false) { + if (is_array($value) === true && array_is_list($value) === false) { // Handle complex filters (after/before) $builder = $this->jsonFilterArray(builder: $builder, filter: $filter, values: $value); continue; diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 86b883d..0dd7915 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -160,7 +160,7 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $schemaObject = $this->schemaMapper->find($schemaId)->getSchemaObject($this->urlGenerator); } - if($schemaObject->properties === []) { + if ($schemaObject->properties === []) { $schemaObject->properties = new stdClass(); } @@ -552,7 +552,7 @@ private function addObject ): string { $subSchema = $schema; - if(is_int($property['$ref']) === true) { + if (is_int($property['$ref']) === true) { $subSchema = $property['$ref']; } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { $parsedUrl = parse_url($property['$ref']); @@ -567,7 +567,7 @@ private function addObject object: $item ); - if($index === null) { + if ($index === null) { // Store relation and replace with reference $relations = $objectEntity->getRelations() ?? []; $relations[$propertyName] = $nestedObject->getUri(); @@ -629,12 +629,12 @@ private function handleArrayProperty( int $schema ): array { - if(isset($property['items']) === false) { + if (isset($property['items']) === false) { return $items; } - if(isset($property['items']['oneOf'])) { - foreach($items as $index=>$item) { + if (isset($property['items']['oneOf']) === true) { + foreach ($items as $index=>$item) { $items[$index] = $this->handleOneOfProperty( property: $property['items']['oneOf'], propertyName: $propertyName, @@ -656,7 +656,7 @@ private function handleArrayProperty( if ($property['items']['type'] === 'file') { - foreach($items as $index => $item) { + foreach ($items as $index => $item) { $items[$index] = $this->handleFileProperty( objectEntity: $objectEntity, object: [$propertyName => [$index => $item]], @@ -666,7 +666,7 @@ private function handleArrayProperty( return $items; } - foreach($items as $index=>$item) { + foreach ($items as $index=>$item) { $items[$index] = $this->addObject( property: $property['items'], propertyName: $propertyName, @@ -921,9 +921,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $fileContent = $response['body']; - if( - $response['encoding'] === 'base64' - ) { + if ($response['encoding'] === 'base64') { $fileContent = base64_decode(string: $fileContent); } From 3167fae5c1dad895e59c59d39ccd055021e8f654 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 12 Dec 2024 14:16:41 +0100 Subject: [PATCH 06/21] Added docblocks --- lib/Db/File.php | 85 ++++++++++++++++++++++++++++++--- lib/Db/FileMapper.php | 106 +++++++++++++++++++++++++++++++----------- 2 files changed, 156 insertions(+), 35 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index 18fdb7c..9957ddc 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -3,26 +3,73 @@ namespace OCA\OpenRegister\Db; use DateTime; +use Exception; use JsonSerializable; use OCP\AppFramework\Db\Entity; use OCP\IURLGenerator; class File extends Entity implements JsonSerializable { - protected string $uuid; - protected string $filename; + /** + * @var string The unique identifier for the file. + */ + protected string $uuid; + + /** + * @var string The name of the file. + */ + protected string $filename; + + /** + * @var string The URL to download the file. + */ protected string $downloadUrl; + + /** + * @var string The URL to share the file. + */ protected string $shareUrl; + + /** + * @var string The URL to access the file. + */ protected string $accessUrl; + + /** + * @var string The file extension (e.g., .txt, .jpg). + */ protected string $extension; + + /** + * @var string The checksum of the file for integrity verification. + */ protected string $checksum; + + /** + * @var string The source of the file. + */ protected string $source; + + /** + * @var string The ID of the user associated with the file. + */ protected string $userId; + + /** + * @var DateTime The date and time when the file was created. + */ protected DateTime $created; + + /** + * @var DateTime The date and time when the file was last updated. + */ protected DateTime $updated; + /** + * Constructor for the File entity. + */ public function __construct() { - $this->addType('uuid', 'string'); + $this->addType('uuid', 'string'); $this->addType('filename', 'string'); $this->addType('downloadUrl', 'string'); $this->addType('shareUrl', 'string'); @@ -35,6 +82,11 @@ public function __construct() { $this->addType('updated', 'datetime'); } + /** + * Retrieves the fields that should be treated as JSON. + * + * @return array List of JSON field names. + */ public function getJsonFields(): array { return array_keys( @@ -44,6 +96,13 @@ public function getJsonFields(): array ); } + /** + * Populates the entity with data from an array. + * + * @param array $object Data to populate the entity. + * + * @return self The hydrated entity. + */ public function hydrate(array $object): self { $jsonFields = $this->getJsonFields(); @@ -57,14 +116,19 @@ public function hydrate(array $object): self try { $this->$method($value); - } catch (\Exception $exception) { -// ("Error writing $key"); + } catch (Exception $exception) { + // Log or handle the exception. } } return $this; } + /** + * Serializes the entity to a JSON-compatible array. + * + * @return array The serialized entity data. + */ public function jsonSerialize(): array { return [ @@ -78,11 +142,18 @@ public function jsonSerialize(): array 'checksum' => $this->checksum, 'source' => $this->source, 'userId' => $this->userId, - 'created' => isset($this->created) ? $this->created->format('c') : null, - 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + 'created' => isset($this->created) === true ? $this->created->format('c') : null, + 'updated' => isset($this->updated) === true ? $this->updated->format('c') : null, ]; } + /** + * Generates a JSON schema for the File entity. + * + * @param IURLGenerator $IURLGenerator The URL generator instance. + * + * @return string The JSON schema as a string. + */ public static function getSchema(IURLGenerator $IURLGenerator): string { return json_encode([ '$id' => $IURLGenerator->getBaseUrl().'/apps/openconnector/api/files/schema', diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index bd7f2b3..c1ada36 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -3,19 +3,37 @@ namespace OCA\OpenRegister\Db; use OCA\OpenRegister\Db\File; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; class FileMapper extends QBMapper { + /** + * Constructor for FileMapper. + * + * @param IDBConnection $db Database connection instance. + */ public function __construct(IDBConnection $db) { parent::__construct($db, 'openconnector_jobs'); } + /** + * Finds a File entity by its ID. + * + * @param int $id The ID of the file to find. + * + * @return \OCA\OpenRegister\Db\File The found file entity. + * @throws Exception If a database error occurs. + * @throws DoesNotExistException If no file is found with the given ID. + * @throws MultipleObjectsReturnedException If multiple files are found with the given ID. + */ public function find(int $id): File { $qb = $this->db->getQueryBuilder(); @@ -29,6 +47,18 @@ public function find(int $id): File return $this->findEntity(query: $qb); } + /** + * Retrieves all File entities with optional filtering, search, and pagination. + * + * @param int|null $limit Maximum number of results to return. + * @param int|null $offset Number of results to skip. + * @param array|null $filters Key-value pairs to filter results. + * @param array|null $searchConditions Search conditions for query. + * @param array|null $searchParams Parameters for search conditions. + * + * @return array List of File entities. + * @throws Exception If a database error occurs. + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -38,7 +68,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setMaxResults($limit) ->setFirstResult($offset); - foreach ($filters as $filter => $value) { + foreach ($filters as $filter => $value) { if ($value === 'IS NOT NULL') { $qb->andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { @@ -46,29 +76,48 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters } else { $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); } - } + } if (empty($searchConditions) === false) { - $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); - foreach ($searchParams as $param => $value) { - $qb->setParameter($param, $value); - } - } + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } return $this->findEntities(query: $qb); } + /** + * Creates a File entity from an array of data. + * + * @param array $object The data to create the entity from. + * + * @return \OCA\OpenRegister\Db\File The created File entity. + * @throws Exception If a database error occurs. + */ public function createFromArray(array $object): File { $obj = new File(); $obj->hydrate($object); - // Set uuid - if ($obj->getUuid() === null){ + // Set UUID + if ($obj->getUuid() === null) { $obj->setUuid(Uuid::v4()); } return $this->insert(entity: $obj); } + /** + * Updates a File entity by its ID using an array of data. + * + * @param int $id The ID of the file to update. + * @param array $object The data to update the entity with. + * + * @return \OCA\OpenRegister\Db\File The updated File entity. + * @throws DoesNotExistException If no file is found with the given ID. + * @throws Exception If a database error occurs. + * @throws MultipleObjectsReturnedException If multiple files are found with the given ID. + */ public function updateFromArray(int $id, array $object): File { $obj = $this->find($id); @@ -82,23 +131,24 @@ public function updateFromArray(int $id, array $object): File return $this->update($obj); } - /** - * Get the total count of all call logs. - * - * @return int The total number of call logs in the database. - */ - public function getTotalCallCount(): int - { - $qb = $this->db->getQueryBuilder(); - - // Select count of all logs - $qb->select($qb->createFunction('COUNT(*) as count')) - ->from('openconnector_jobs'); - - $result = $qb->execute(); - $row = $result->fetch(); - - // Return the total count - return (int) $row['count']; - } + /** + * Gets the total count of all call logs. + * + * @return int The total number of call logs in the database. + * @throws Exception If a database error occurs. + */ + public function getTotalCallCount(): int + { + $qb = $this->db->getQueryBuilder(); + + // Select count of all logs + $qb->select($qb->createFunction('COUNT(*) as count')) + ->from('openconnector_jobs'); + + $result = $qb->execute(); + $row = $result->fetch(); + + // Return the total count + return (int) $row['count']; + } } From d00ce52e2cffc39c61149e057e9a6473cd69460e Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 12 Dec 2024 14:44:53 +0100 Subject: [PATCH 07/21] Some more small docblock updates --- lib/Service/ObjectService.php | 175 +++++++++++++++++----------------- 1 file changed, 86 insertions(+), 89 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 0dd7915..f92ac82 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -6,24 +6,20 @@ use Exception; use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; -use OC\URLGenerator; +use JsonSerializable; use OCA\OpenRegister\Db\File; -use OCA\OpenRegister\Db\Source; -use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\ObjectEntityMapper; -use OCA\OpenRegister\Db\AuditTrail; use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Formats\BsnFormat; use OCP\App\IAppManager; use OCP\IAppConfig; use OCP\IURLGenerator; -use Opis\JsonSchema\Errors\ErrorFormatter; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; use Opis\Uri\Uri; @@ -53,36 +49,34 @@ class ObjectService /** @var int The current schema ID */ private int $schema; - /** @var AuditTrailMapper For tracking object changes */ - private AuditTrailMapper $auditTrailMapper; + /** + * Constructor for ObjectService + * + * Initializes the service with required mappers for database operations + * + * @param ObjectEntityMapper $objectEntityMapper + * @param RegisterMapper $registerMapper + * @param SchemaMapper $schemaMapper + * @param AuditTrailMapper $auditTrailMapper + * @param ContainerInterface $container + * @param IURLGenerator $urlGenerator + * @param FileService $fileService + * @param IAppManager $appManager + * @param IAppConfig $config + */ + public function __construct( + private readonly ObjectEntityMapper $objectEntityMapper, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly ContainerInterface $container, + private readonly IURLGenerator $urlGenerator, + private readonly FileService $fileService, + private readonly IAppManager $appManager, + private readonly IAppConfig $config, + ) { - /** - * Constructor for ObjectService - * - * Initializes the service with required mappers for database operations - * - * @param ObjectEntityMapper $objectEntityMapper Mapper for object entities - * @param RegisterMapper $registerMapper Mapper for registers - * @param SchemaMapper $schemaMapper Mapper for schemas - * @param AuditTrailMapper $auditTrailMapper Mapper for audit trails - */ - public function __construct( - ObjectEntityMapper $objectEntityMapper, - RegisterMapper $registerMapper, - SchemaMapper $schemaMapper, - AuditTrailMapper $auditTrailMapper, - private ContainerInterface $container, - private readonly IURLGenerator $urlGenerator, - private readonly FileService $fileService, - private readonly IAppManager $appManager, - private readonly IAppConfig $config, - ) - { - $this->objectEntityMapper = $objectEntityMapper; - $this->registerMapper = $registerMapper; - $this->schemaMapper = $schemaMapper; - $this->auditTrailMapper = $auditTrailMapper; - } + } /** * Attempts to retrieve the OpenConnector service from the container. @@ -111,7 +105,6 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m * @param Uri $uri The URI registered by the resolver. * * @return string The resulting json object. - * * @throws GuzzleException */ public function resolveSchema(Uri $uri): string @@ -199,7 +192,7 @@ public function find(int|string $id, ?array $extend = []): ObjectEntity * @param array $object The object data * * @return ObjectEntity The created object - * @throws ValidationException + * @throws ValidationException|GuzzleException */ public function createFromArray(array $object): ObjectEntity { @@ -218,7 +211,7 @@ public function createFromArray(array $object): ObjectEntity * @param bool $updatedObject Whether this is an update operation * * @return ObjectEntity The updated object - * @throws ValidationException + * @throws ValidationException|GuzzleException */ public function updateFromArray(string $id, array $object, bool $updatedObject, bool $patch = false): ObjectEntity { @@ -242,15 +235,15 @@ public function updateFromArray(string $id, array $object, bool $updatedObject, /** * Delete an object * - * @param array|\JsonSerializable $object The object to delete + * @param array|JsonSerializable $object The object to delete * * @return bool True if deletion was successful * @throws Exception */ - public function delete(array|\JsonSerializable $object): bool + public function delete(array|JsonSerializable $object): bool { // Convert JsonSerializable objects to array - if ($object instanceof \JsonSerializable === true) { + if ($object instanceof JsonSerializable === true) { $object = $object->jsonSerialize(); } @@ -275,17 +268,15 @@ public function delete(array|\JsonSerializable $object): bool */ public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array { - $objects = $this->getObjects( - register: $this->getRegister(), - schema: $this->getSchema(), - limit: $limit, - offset: $offset, - filters: $filters, - sort: $sort, - search: $search - ); - - return $objects; + return $this->getObjects( + register: $this->getRegister(), + schema: $this->getSchema(), + limit: $limit, + offset: $offset, + filters: $filters, + sort: $sort, + search: $search + ); } /** @@ -293,6 +284,7 @@ public function findAll(?int $limit = null, ?int $offset = null, array $filters * * @param array $filters Filter criteria * @param string|null $search Search term + * * @return int Total count */ public function count(array $filters = [], ?string $search = null): int @@ -402,7 +394,7 @@ public function getObjects(?string $objectType = null, ?int $register = null, ?i * * @return ObjectEntity The resulting object. * @throws ValidationException When the validation fails and returns an error. - * @throws Exception + * @throws Exception|GuzzleException */ public function saveObject(int $register, int $schema, array $object): ObjectEntity { @@ -478,16 +470,15 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt } /** - * 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 - */ + * 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 + * + * @return ObjectEntity Updated object data + */ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity { $relations = $objectEntity->getRelations() ?? []; @@ -584,14 +575,15 @@ private function addObject /** * Handles a property that is of the type array. * - * @param array $property The property to handle - * @param string $propertyName The name of the property - * @param array $item The contents of the property + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $item The contents of the property * @param ObjectEntity $objectEntity The objectEntity the data belongs to - * @param int $register The register connected to the objectEntity - * @param int $schema The schema connected to the objectEntity + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity * * @return string The updated item + * @throws ValidationException */ private function handleObjectProperty( array $property, @@ -618,7 +610,7 @@ private function handleObjectProperty( * @param int $schema The schema connected to the objectEntity * * @return array The updated item - * @throws GuzzleException + * @throws GuzzleException|ValidationException */ private function handleArrayProperty( array $property, @@ -693,7 +685,7 @@ private function handleArrayProperty( * @param int|null $index If the oneOf is in an array, the index within the array * * @return string|array The updated item - * @throws GuzzleException + * @throws GuzzleException|ValidationException */ private function handleOneOfProperty( array $property, @@ -768,7 +760,7 @@ private function handleOneOfProperty( * @param ObjectEntity $objectEntity The objectEntity to write the object in * * @return array The resulting object - * @throws GuzzleException + * @throws GuzzleException|ValidationException */ private function handleProperty ( array $property, @@ -833,7 +825,7 @@ private function handleProperty ( * @param int $schema The schema ID * * @return ObjectEntity Updated object with linked data - * @throws Exception|ValidationException When file handling fails + * @throws Exception|ValidationException|GuzzleException When file handling fails */ private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): ObjectEntity { @@ -981,17 +973,17 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s return $object; } - /** - * Get an object - * - * @param Register $register The register to get the object from - * @param Schema $schema The schema of the object - * @param string $uuid The UUID of the object to get - * @param array $extend Properties to extend with related data - * - * @return ObjectEntity The resulting object - * @throws Exception If source type is unsupported - */ + /** + * Get an object + * + * @param Register $register The register to get the object from + * @param Schema $schema The schema of the object + * @param string $uuid The UUID of the object to get + * @param array|null $extend Properties to extend with related data + * + * @return ObjectEntity The resulting object + * @throws Exception If source type is unsupported + */ public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity { @@ -1035,6 +1027,7 @@ public function deleteObject(Register $register, Schema $schema, string $uuid): * @param string|null $objectType The type of object to retrieve the mapper for * @param int|null $register Optional register ID * @param int|null $schema Optional schema ID + * * @return mixed The appropriate mapper * @throws InvalidArgumentException If unknown object type */ @@ -1065,6 +1058,7 @@ public function getMapper(?string $objectType = null, ?int $register = null, ?in * * @param string $objectType The type of objects to retrieve * @param array $ids The ids of the objects to retrieve + * * @return array The retrieved objects * @throws InvalidArgumentException If unknown object type */ @@ -1095,13 +1089,15 @@ public function getMultipleObjects(string $objectType, array $ids): array return $mapper->findMultiple($cleanedIds); } - /** - * Renders the entity by replacing the files and relations with their respective objects - * - * @param array $entity The entity to render - * @param array|null $extend Optional array of properties to extend, defaults to files and relations if not provided - * @return array The rendered entity with expanded files and relations - */ + /** + * Renders the entity by replacing the files and relations with their respective objects + * + * @param array $entity The entity to render + * @param array|null $extend Optional array of properties to extend, defaults to files and relations if not provided + * + * @return array The rendered entity with expanded files and relations + * @throws Exception + */ public function renderEntity(array $entity, ?array $extend = []): array { // check if entity has files or relations and if not just return the entity @@ -1145,6 +1141,7 @@ public function renderEntity(array $entity, ?array $extend = []): array * * @param mixed $entity The entity to extend * @param array $extend Properties to extend with related data + * * @return array The extended entity as an array * @throws Exception If property not found or no mapper available */ From a5751d7379eedca321648a454de976801f245eda Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 12 Dec 2024 15:36:23 +0100 Subject: [PATCH 08/21] Big docblock update for ObjectService --- lib/Service/ObjectService.php | 890 ++++++++++++++++++---------------- 1 file changed, 470 insertions(+), 420 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index f92ac82..d2c74ac 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -31,38 +31,38 @@ use GuzzleHttp\Client; /** - * Service class for handling object operations + * Service class for handling object operations. * - * This service provides methods for CRUD operations on objects, including: - * - Creating, reading, updating and deleting objects - * - Finding objects by ID/UUID - * - Getting audit trails - * - Extending objects with related data + * This service provides methods for: + * - CRUD operations on objects. + * - Schema resolution and validation. + * - Managing relations and linked data (extending objects with related sub-objects). + * - Audit trails and data aggregation. * * @package OCA\OpenRegister\Service */ class ObjectService { - /** @var int The current register ID */ - private int $register; + /** @var int The current register ID */ + private int $register; - /** @var int The current schema ID */ - private int $schema; + /** @var int The current schema ID */ + private int $schema; /** - * Constructor for ObjectService - * - * Initializes the service with required mappers for database operations - * - * @param ObjectEntityMapper $objectEntityMapper - * @param RegisterMapper $registerMapper - * @param SchemaMapper $schemaMapper - * @param AuditTrailMapper $auditTrailMapper - * @param ContainerInterface $container - * @param IURLGenerator $urlGenerator - * @param FileService $fileService - * @param IAppManager $appManager - * @param IAppConfig $config + * Constructor for ObjectService. + * + * Initializes the service with dependencies required for database and object operations. + * + * @param ObjectEntityMapper $objectEntityMapper Object entity data mapper. + * @param RegisterMapper $registerMapper Register data mapper. + * @param SchemaMapper $schemaMapper Schema data mapper. + * @param AuditTrailMapper $auditTrailMapper Audit trail data mapper. + * @param ContainerInterface $container Dependency injection container. + * @param IURLGenerator $urlGenerator URL generator service. + * @param FileService $fileService File service for managing files. + * @param IAppManager $appManager Application manager service. + * @param IAppConfig $config Configuration manager. */ public function __construct( private readonly ObjectEntityMapper $objectEntityMapper, @@ -73,25 +73,25 @@ public function __construct( private readonly IURLGenerator $urlGenerator, private readonly FileService $fileService, private readonly IAppManager $appManager, - private readonly IAppConfig $config, - ) { - + private readonly IAppConfig $config + ) { } /** - * Attempts to retrieve the OpenConnector service from the container. + * Retrieves the OpenConnector service from the container. * - * @return mixed|null The OpenConnector service if available, null otherwise. - * @throws ContainerExceptionInterface|NotFoundExceptionInterface + * @param string $filePath Optional file path for the OpenConnector service. + * + * @return mixed|null The OpenConnector service instance or null if not available. + * @throws ContainerExceptionInterface If there is a container exception. + * @throws NotFoundExceptionInterface If the service is not found. */ public function getOpenConnector(string $filePath = '\Service\ObjectService'): mixed { - if (in_array(needle: 'openconnector', haystack: $this->appManager->getInstalledApps()) === true) { + if (in_array('openconnector', $this->appManager->getInstalledApps())) { try { - // Attempt to get a OpenConnector file from the container return $this->container->get("OCA\OpenConnector$filePath"); } catch (Exception $e) { - // If the file is not available, return null return null; } } @@ -100,34 +100,34 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m } /** - * Fetch schema from URL + * Resolves a schema from a given URI. * - * @param Uri $uri The URI registered by the resolver. + * @param Uri $uri The URI pointing to the schema. * - * @return string The resulting json object. - * @throws GuzzleException + * @return string The schema content in JSON format. + * @throws GuzzleException If there is an error during schema fetching. */ public function resolveSchema(Uri $uri): string { - if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() - && str_contains(haystack: $uri->path(), needle: '/api/schemas') === true + // Local schema resolution + if ($this->urlGenerator->getBaseUrl() === $uri->scheme() . '://' . $uri->host() + && str_contains($uri->path(), '/api/schemas') ) { - $exploded = explode(separator: '/', string: $uri->path()); - $schema = $this->schemaMapper->find(end($exploded)); + $exploded = explode('/', $uri->path()); + $schema = $this->schemaMapper->find(end($exploded)); return json_encode($schema->getSchemaObject($this->urlGenerator)); } - if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() - && str_contains(haystack: $uri->path(), needle: '/api/files/schema') === true + // File schema resolution + if ($this->urlGenerator->getBaseUrl() === $uri->scheme() . '://' . $uri->host() + && str_contains($uri->path(), '/api/files/schema') ) { - $exploded = explode(separator: '/', string: $uri->path()); return File::getSchema($this->urlGenerator); } - // @TODO: Validate file schema - - if ($this->config->getValueBool(app: 'openregister', key: 'allowExternalSchemas') === true) { + // External schema resolution + if ($this->config->getValueBool('openregister', 'allowExternalSchemas')) { $client = new Client(); $result = $client->get(\GuzzleHttp\Psr7\Uri::fromParts($uri->components())); @@ -138,14 +138,13 @@ public function resolveSchema(Uri $uri): string } /** - * Validate an object with a schema. - * If schema is not given and schemaObject is filled, the object will validate to the schemaObject. + * Validates an object against a schema. * - * @param array $object The object to validate. - * @param int|null $schemaId The id of the schema to validate to. - * @param object $schemaObject A schema object to validate to. + * @param array $object The object to validate. + * @param int|null $schemaId The schema ID to validate against. + * @param object $schemaObject A custom schema object for validation. * - * @return ValidationResult The validation result from opis/json-schema. + * @return ValidationResult The result of the validation. */ public function validateObject(array $object, ?int $schemaId = null, object $schemaObject = new stdClass()): ValidationResult { @@ -153,121 +152,119 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $schemaObject = $this->schemaMapper->find($schemaId)->getSchemaObject($this->urlGenerator); } - if ($schemaObject->properties === []) { - $schemaObject->properties = new stdClass(); - } - $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); $validator->loader()->resolver()->registerProtocol('http', [$this, 'resolveSchema']); - - return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); - + return $validator->validate(json_decode(json_encode($object)), $schemaObject); } /** - * Find an object by ID or UUID + * Finds an object by ID or UUID. * - * @param int|string $id The ID or UUID to search for - * @param array|null $extend Properties to extend with related data + * @param int|string $id The object ID or UUID. + * @param array|null $extend Properties to extend the object with. * - * @return ObjectEntity The found object - * @throws Exception + * @return ObjectEntity The found object entity. + * @throws Exception If the object is not found. */ - public function find(int|string $id, ?array $extend = []): ObjectEntity + public function find(int|string $id, ?array $extend = []): ObjectEntity { - return $this->getObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $id, - extend: $extend - ); - } + return $this->getObject( + $this->registerMapper->find($this->getRegister()), + $this->schemaMapper->find($this->getSchema()), + $id, + $extend + ); + } /** - * Create a new object from array data + * Creates a new object from provided data. * - * @param array $object The object data + * @param array $object The object data. * - * @return ObjectEntity The created object - * @throws ValidationException|GuzzleException + * @return ObjectEntity The created object entity. + * @throws ValidationException If validation fails. + * @throws GuzzleException If there is an error during file upload. */ - public function createFromArray(array $object): ObjectEntity + public function createFromArray(array $object): ObjectEntity { - return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } + return $this->saveObject( + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } /** - * Update an existing object from array data + * Updates an existing object with new data. * - * @param string $id The object ID to update - * @param array $object The new object data - * @param bool $updatedObject Whether this is an update operation + * @param string $id The ID of the object to update. + * @param array $object The new data for the object. + * @param bool $updatedObject If true, performs a full update. If false, performs a patch update. + * @param bool $patch Determines if the update should merge with existing data. * - * @return ObjectEntity The updated object - * @throws ValidationException|GuzzleException + * @return ObjectEntity The updated object entity. + * @throws ValidationException If validation fails. + * @throws GuzzleException If there is an error during file upload. */ - public function updateFromArray(string $id, array $object, bool $updatedObject, bool $patch = false): ObjectEntity + public function updateFromArray(string $id, array $object, bool $updatedObject, bool $patch = false): ObjectEntity { - // Add ID to object data for update - $object['id'] = $id; + $object['id'] = $id; - // If we want the update to behave like patch, merge with existing object. if ($patch === true) { - $oldObject = $this->getObject($this->registerMapper->find($this->getRegister()), $this->schemaMapper->find($this->getSchema()), $id)->jsonSerialize(); + $oldObject = $this->getObject( + $this->registerMapper->find($this->getRegister()), + $this->schemaMapper->find($this->getSchema()), + $id + )->jsonSerialize(); $object = array_merge($oldObject, $object); } return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } /** - * Delete an object + * Deletes an object. * - * @param array|JsonSerializable $object The object to delete + * @param array|JsonSerializable $object The object to delete. * - * @return bool True if deletion was successful - * @throws Exception + * @return bool True if deletion is successful, false otherwise. + * @throws Exception If deletion fails. */ - public function delete(array|JsonSerializable $object): bool - { - // Convert JsonSerializable objects to array - if ($object instanceof JsonSerializable === true) { - $object = $object->jsonSerialize(); - } + public function delete(array|JsonSerializable $object): bool + { + if ($object instanceof JsonSerializable) { + $object = $object->jsonSerialize(); + } - return $this->deleteObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $object['id'] - ); - } + return $this->deleteObject( + register: $this->registerMapper->find($this->getRegister()), + schema: $this->schemaMapper->find($this->getSchema()), + uuid: $object['id'] + ); + } /** - * Find all objects matching given criteria + * Retrieves all objects matching criteria. * - * @param int|null $limit Maximum number of results - * @param int|null $offset Starting offset for pagination - * @param array $filters Filter criteria - * @param array $sort Sorting criteria - * @param string|null $search Search term - * @param array|null $extend Properties to extend with related data + * @param int|null $limit Maximum number of results. + * @param int|null $offset Starting offset for pagination. + * @param array $filters Criteria to filter the objects. + * @param array $sort Sorting options. + * @param string|null $search Search term. + * @param array|null $extend Properties to extend the results with. * - * @return array List of matching objects + * @return array List of matching objects. */ - public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array - { + public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array + { return $this->getObjects( register: $this->getRegister(), schema: $this->getSchema(), @@ -277,124 +274,136 @@ public function findAll(?int $limit = null, ?int $offset = null, array $filters sort: $sort, search: $search ); - } + } - /** - * Count total objects matching filters - * - * @param array $filters Filter criteria - * @param string|null $search Search term + /** + * Counts the total number of objects matching criteria. * - * @return int Total count - */ - public function count(array $filters = [], ?string $search = null): int - { - // Add register and schema filters if set - if ($this->getSchema() !== null && $this->getRegister() !== null) { - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); - } + * @param array $filters Criteria to filter the objects. + * @param string|null $search Search term. + * + * @return int The total count of matching objects. + */ + public function count(array $filters = [], ?string $search = null): int + { + // Add register and schema filters if set + if ($this->getSchema() !== null && $this->getRegister() !== null) { + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + } - return $this->objectEntityMapper - ->countAll(filters: $filters, search: $search); - } + return $this->objectEntityMapper + ->countAll(filters: $filters, search: $search); + } /** - * Find multiple objects by their IDs + * Retrieves multiple objects by their IDs. * - * @param array $ids Array of object IDs to find + * @param array $ids List of object IDs to retrieve. * - * @return array Array of found objects - * @throws Exception + * @return array List of retrieved objects. + * @throws Exception If an error occurs during retrieval. */ - public function findMultiple(array $ids): array - { - $result = []; - foreach ($ids as $id) { - $result[] = $this->find($id); - } + public function findMultiple(array $ids): array + { + $result = []; + foreach ($ids as $id) { + $result[] = $this->find($id); + } - return $result; - } + return $result; + } - /** - * Get aggregations for objects matching filters - * - * @param array $filters Filter criteria - * @param string|null $search Search term + /** + * Retrieves aggregation data based on filters and criteria. * - * @return array Aggregation results - */ - public function getAggregations(array $filters, ?string $search = null): array - { - $mapper = $this->getMapper(objectType: 'objectEntity'); + * @param array $filters Criteria to filter objects. + * @param string|null $search Search term. + * + * @return array Aggregated data results. + */ + public function getAggregations(array $filters, ?string $search = null): array + { + $mapper = $this->getMapper(objectType: 'objectEntity'); - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); - // Only ObjectEntityMapper supports facets - if ($mapper instanceof ObjectEntityMapper === true) { - $facets = $this->objectEntityMapper->getFacets($filters, $search); - return $facets; - } + if ($mapper instanceof ObjectEntityMapper) { + return $mapper->getFacets($filters, $search); + } - return []; - } + return []; + } /** - * Extract object data from an entity + * Extracts object data from an entity. * - * @param mixed $object The object to extract data from - * @param array|null $extend Properties to extend with related data + * @param mixed $object The object entity. + * @param array|null $extend Properties to extend the object data with. * - * @return mixed The extracted object data + * @return mixed The extracted object data. */ - private function getDataFromObject(mixed $object, ?array $extend = []): mixed + private function getDataFromObject(mixed $object, ?array $extend = []): mixed { - return $object->getObject(); - } + return $object->getObject(); + } /** - * Gets all objects of a specific type. - * - * @param string|null $objectType The type of objects to retrieve. - * @param int|null $register - * @param int|null $schema - * @param int|null $limit The maximum number of objects to retrieve. - * @param int|null $offset The offset from which to start retrieving objects. - * @param array $filters - * @param array $sort - * @param string|null $search - * @param array|null $extend Properties to extend with related data + * Retrieves all objects of a specified type. * - * @return array The retrieved objects. + * @param string|null $objectType The type of objects to retrieve. Defaults to 'objectEntity' if register and schema are provided. + * @param int|null $register The ID of the register to filter objects by. + * @param int|null $schema The ID of the schema to filter objects by. + * @param int|null $limit The maximum number of objects to retrieve. Null for no limit. + * @param int|null $offset The offset for pagination. Null for no offset. + * @param array $filters Additional filters for retrieving objects. + * @param array $sort Sorting criteria for the retrieved objects. + * @param string|null $search Search term for filtering objects. + * @param array|null $extend Properties to extend with related data. + * + * @return array An array of objects matching the specified criteria. + * @throws InvalidArgumentException If an invalid object type is specified. */ - public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array - { - // Set object type and filters if register and schema are provided - if ($objectType === null && $register !== null && $schema !== null) { - $objectType = 'objectEntity'; - $filters['register'] = $register; - $filters['schema'] = $schema; - } + public function getObjects( + ?string $objectType = null, + ?int $register = null, + ?int $schema = null, + ?int $limit = null, + ?int $offset = null, + array $filters = [], + array $sort = [], + ?string $search = null, + ?array $extend = [] + ): array + { + if ($objectType === null && $register !== null && $schema !== null) { + $objectType = 'objectEntity'; + $filters['register'] = $register; + $filters['schema'] = $schema; + } - // Get the appropriate mapper for the object type - $mapper = $this->getMapper($objectType); + $mapper = $this->getMapper($objectType); - // Use the mapper to find and return all objects of the specified type - return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); - } + return $mapper->findAll( + limit: $limit, + offset: $offset, + filters: $filters, + sort: $sort, + search: $search + ); + } - /** - * Save an object + /** + * Saves an object to the database. * - * @param int $register The register to save the object to. - * @param int $schema The schema to save the object to. - * @param array $object The data to be saved. + * @param int $register The ID of the register to save the object to. + * @param int $schema The ID of the schema to save the object to. + * @param array $object The data of the object to save. * - * @return ObjectEntity The resulting object. - * @throws ValidationException When the validation fails and returns an error. - * @throws Exception|GuzzleException + * @return ObjectEntity The saved object entity. + * @throws ValidationException If the object fails validation. + * @throws Exception|GuzzleException If an error occurs during object saving or file handling. */ public function saveObject(int $register, int $schema, array $object): ObjectEntity { @@ -470,14 +479,14 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt } /** - * Handle link relations efficiently using JSON path traversal + * Efficiently processes link relations within an object 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 + * Identifies and maps all URLs or UUIDs to their corresponding relations using dot notation paths + * for nested properties, excluding self-references of the object entity. * - * @param ObjectEntity $objectEntity The object entity to handle relations for + * @param ObjectEntity $objectEntity The object entity to analyze and update relations for. * - * @return ObjectEntity Updated object data + * @return ObjectEntity The updated object entity with new relations mapped. */ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity { @@ -518,28 +527,31 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity } /** - * Adds a subobject based upon given parameters and adds it to the main object. - * - * @param array $property The property to handle - * @param string $propertyName The name of the property - * @param array $item The contents of the property - * @param ObjectEntity $objectEntity The objectEntity the data belongs to - * @param int $register The register connected to the objectEntity - * @param int $schema The schema connected to the objectEntity - * @param int|null $index If the subobject is in an array, the index of the object in the array. - * - * @return string The updated item - * @throws ValidationException + * Adds a nested subobject based on schema and property details and incorporates it into the main object. + * + * Handles $ref resolution for schema subtypes, stores relations, and replaces nested subobject + * data with its reference URI or UUID. + * + * @param array $property The property schema details for the nested object. + * @param string $propertyName The name of the property in the parent object. + * @param array $item The nested subobject data to process. + * @param ObjectEntity $objectEntity The parent object entity to associate the nested subobject with. + * @param int $register The register associated with the schema. + * @param int $schema The schema identifier for the subobject. + * @param int|null $index Optional index of the subobject if it resides in an array. + * + * @return string The UUID of the nested subobject. + * @throws ValidationException When schema or object validation fails. + * @throws GuzzleException */ - private function addObject - ( - array $property, - string $propertyName, - array $item, + private function addObject( + array $property, + string $propertyName, + array $item, ObjectEntity $objectEntity, - int $register, - int $schema, - ?int $index = null, + int $register, + int $schema, + ?int $index = null ): string { $subSchema = $schema; @@ -573,52 +585,61 @@ private function addObject } /** - * Handles a property that is of the type array. + * Processes an object property by delegating it to a subobject handling mechanism. * - * @param array $property The property to handle - * @param string $propertyName The name of the property - * @param array $item The contents of the property - * @param ObjectEntity $objectEntity The objectEntity the data belongs to - * @param int $register The register connected to the objectEntity - * @param int $schema The schema connected to the objectEntity + * @param array $property The schema definition for the object property. + * @param string $propertyName The name of the object property. + * @param array $item The data corresponding to the property in the parent object. + * @param ObjectEntity $objectEntity The object entity to link the processed data to. + * @param int $register The register associated with the schema. + * @param int $schema The schema identifier for the property. * - * @return string The updated item - * @throws ValidationException + * @return string The updated property data, typically a reference UUID. + * @throws ValidationException When schema or object validation fails. + * @throws GuzzleException */ private function handleObjectProperty( - array $property, - string $propertyName, - array $item, + array $property, + string $propertyName, + array $item, ObjectEntity $objectEntity, - int $register, - int $schema + int $register, + int $schema ): string { return $this->addObject( - property: $property,propertyName: $propertyName, item: $item, objectEntity: $objectEntity, register: $register, schema: $schema + property: $property, + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema ); } /** - * Handles a property that is of the type array. + * Handles array-type properties by processing each element based on its schema type. * - * @param array $property The property to handle - * @param string $propertyName The name of the property - * @param array $items The contents of the property - * @param ObjectEntity $objectEntity The objectEntity the data belongs to - * @param int $register The register connected to the objectEntity - * @param int $schema The schema connected to the objectEntity + * Supports nested objects, files, or oneOf schema types, delegating to specific handlers + * for each element in the array. * - * @return array The updated item - * @throws GuzzleException|ValidationException + * @param array $property The schema definition for the array property. + * @param string $propertyName The name of the array property. + * @param array $items The elements of the array to process. + * @param ObjectEntity $objectEntity The object entity the data belongs to. + * @param int $register The register associated with the schema. + * @param int $schema The schema identifier for the array elements. + * + * @return array The processed array with updated references or data. + * @throws GuzzleException|ValidationException When schema validation or file handling fails. */ private function handleArrayProperty( - array $property, - string $propertyName, - array $items, + array $property, + string $propertyName, + array $items, ObjectEntity $objectEntity, - int $register, - int $schema + int $register, + int $schema ): array { if (isset($property['items']) === false) { @@ -674,27 +695,30 @@ private function handleArrayProperty( } /** - * Handles a property that of the type oneOf. - * - * @param array $property The property to handle - * @param string $propertyName The name of the property - * @param string|array $item The contents of the property - * @param ObjectEntity $objectEntity The objectEntity the data belongs to - * @param int $register The register connected to the objectEntity - * @param int $schema The schema connected to the objectEntity - * @param int|null $index If the oneOf is in an array, the index within the array - * - * @return string|array The updated item - * @throws GuzzleException|ValidationException + * Processes properties defined as oneOf, selecting the appropriate schema option for the data. + * + * Handles various types of schemas, including files and references, to correctly process + * and replace the input data with the resolved references or processed results. + * + * @param array $property The oneOf schema definition. + * @param string $propertyName The name of the property in the parent object. + * @param string|array $item The data to process, either as a scalar or a nested array. + * @param ObjectEntity $objectEntity The object entity the data belongs to. + * @param int $register The register associated with the schema. + * @param int $schema The schema identifier for the property. + * @param int|null $index Optional index for array-based oneOf properties. + * + * @return string|array The processed data, resolved to a reference or updated structure. + * @throws GuzzleException|ValidationException When schema validation or file handling fails. */ private function handleOneOfProperty( - array $property, - string $propertyName, + array $property, + string $propertyName, string|array $item, ObjectEntity $objectEntity, - int $register, - int $schema, - ?int $index = null + int $register, + int $schema, + ?int $index = null ): string|array { if (array_is_list($property) === false) { @@ -749,20 +773,22 @@ private function handleOneOfProperty( } /** - * Rewrites subobjects stored in separate objectentities to the Uuid of that object, - * rewrites files to the chosen format - * - * @param array $property The content of the property in the schema - * @param string $propertyName The name of the property - * @param int $register The register the main object is in - * @param int $schema The schema of the main object - * @param array $object The object to rewrite - * @param ObjectEntity $objectEntity The objectEntity to write the object in - * - * @return array The resulting object - * @throws GuzzleException|ValidationException + * Processes and rewrites properties within an object based on their schema definitions. + * + * Determines the type of each property (object, array, oneOf, or file) and delegates to the + * corresponding handler. Updates the object data with references or processed results. + * + * @param array $property The schema definition of the property. + * @param string $propertyName The name of the property in the object. + * @param int $register The register ID associated with the schema. + * @param int $schema The schema ID associated with the property. + * @param array $object The parent object data to update. + * @param ObjectEntity $objectEntity The object entity being processed. + * + * @return array The updated object with processed properties. + * @throws GuzzleException|ValidationException When schema validation or file handling fails. */ - private function handleProperty ( + private function handleProperty( array $property, string $propertyName, int $register, @@ -816,18 +842,27 @@ private function handleProperty ( /** - * Handle object relations and file properties in schema properties and array items + * Links object relations and handles file-based properties within an object schema. * - * @param ObjectEntity $objectEntity The object entity to handle relations for - * @param array $object The object data - * @param array $properties The schema properties - * @param int $register The register ID - * @param int $schema The schema ID + * Iterates through schema-defined properties, processing and resolving nested relations, + * array items, and file-based data. Updates the object entity with resolved references. * - * @return ObjectEntity Updated object with linked data - * @throws Exception|ValidationException|GuzzleException When file handling fails + * @param ObjectEntity $objectEntity The object entity being processed. + * @param array $object The parent object data to analyze. + * @param array $properties The schema properties defining the object structure. + * @param int $register The register ID associated with the schema. + * @param int $schema The schema ID associated with the object. + * + * @return ObjectEntity The updated object entity with resolved relations and file references. + * @throws Exception|ValidationException|GuzzleException When file handling or schema processing fails. */ - private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): ObjectEntity + 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) { @@ -852,14 +887,17 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } /** - * Handle file property processing + * Processes file properties within an object, storing and resolving file content to sharable URLs. + * + * Handles both base64-encoded and URL-based file sources, storing the resolved content and + * updating the object data with the resulting file references. * - * @param ObjectEntity $objectEntity The object entity - * @param array $object The object data - * @param string $propertyName The name of the file property + * @param ObjectEntity $objectEntity The object entity containing the file property. + * @param array $object The parent object data containing the file reference. + * @param string $propertyName The name of the file property. * - * @return array Updated object data - * @throws Exception|GuzzleException When file handling fails + * @return array The updated object with resolved file references. + * @throws Exception|GuzzleException When file processing or storage fails. */ private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { @@ -974,18 +1012,20 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } /** - * Get an object + * Retrieves an object from a specified register and schema using its UUID. * - * @param Register $register The register to get the object from - * @param Schema $schema The schema of the object - * @param string $uuid The UUID of the object to get - * @param array|null $extend Properties to extend with related data + * Supports only internal sources and raises an exception for unsupported source types. * - * @return ObjectEntity The resulting object - * @throws Exception If source type is unsupported + * @param Register $register The register from which the object is retrieved. + * @param Schema $schema The schema defining the object structure. + * @param string $uuid The unique identifier of the object to retrieve. + * @param array|null $extend Optional properties to include in the retrieved object. + * + * @return ObjectEntity The retrieved object as an entity. + * @throws Exception If the source type is unsupported. */ - public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity - { + public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity + { // Handle internal source if ($register->getSource() === 'internal' || $register->getSource() === '') { @@ -997,18 +1037,20 @@ public function getObject(Register $register, Schema $schema, string $uuid, ?arr throw new Exception('Unsupported source type'); } - /** - * Delete an object - * - * @param Register $register The register to delete from - * @param Schema $schema The schema of the object - * @param string $uuid The UUID of the object to delete - * - * @return bool True if deletion was successful - * @throws Exception If source type is unsupported - */ - public function deleteObject(Register $register, Schema $schema, string $uuid): bool - { + /** + * Deletes an object from a specified register and schema using its UUID. + * + * Supports only internal sources and raises an exception for unsupported source types. + * + * @param Register $register The register containing the object to delete. + * @param Schema $schema The schema defining the object structure. + * @param string $uuid The unique identifier of the object to delete. + * + * @return bool True if the object was successfully deleted. + * @throws Exception If the source type is unsupported. + */ + public function deleteObject(Register $register, Schema $schema, string $uuid): bool + { // Handle internal source if ($register->getSource() === 'internal' || $register->getSource() === '') { $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); @@ -1021,18 +1063,20 @@ public function deleteObject(Register $register, Schema $schema, string $uuid): throw new Exception('Unsupported source type'); } - /** - * Gets the appropriate mapper based on the object type. - * - * @param string|null $objectType The type of object to retrieve the mapper for - * @param int|null $register Optional register ID - * @param int|null $schema Optional schema ID - * - * @return mixed The appropriate mapper - * @throws InvalidArgumentException If unknown object type - */ - public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null): mixed - { + /** + * Retrieves the appropriate mapper for a specific object type. + * + * Optionally sets the current register and schema when both are provided. + * + * @param string|null $objectType The type of the object for which a mapper is needed. + * @param int|null $register Optional register ID to set for the mapper. + * @param int|null $schema Optional schema ID to set for the mapper. + * + * @return mixed The mapper for the specified object type. + * @throws InvalidArgumentException If the object type is unknown. + */ + public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null): mixed + { // Return self if register and schema provided if ($register !== null && $schema !== null) { $this->setSchema($schema); @@ -1053,16 +1097,18 @@ public function getMapper(?string $objectType = null, ?int $register = null, ?in } } - /** - * Gets multiple objects based on the object type and ids. - * - * @param string $objectType The type of objects to retrieve - * @param array $ids The ids of the objects to retrieve + /** + * Retrieves multiple objects of a specified type using their identifiers. + * + * Processes and cleans input IDs to ensure compatibility with the mapper. * - * @return array The retrieved objects - * @throws InvalidArgumentException If unknown object type - */ - public function getMultipleObjects(string $objectType, array $ids): array + * @param string $objectType The type of objects to retrieve. + * @param array $ids The list of object IDs to retrieve. + * + * @return array The retrieved objects. + * @throws InvalidArgumentException If the object type is unknown. + */ + public function getMultipleObjects(string $objectType, array $ids): array { // Process the ids to handle different formats $processedIds = array_map(function($id) { @@ -1090,16 +1136,18 @@ public function getMultipleObjects(string $objectType, array $ids): array } /** - * Renders the entity by replacing the files and relations with their respective objects + * Renders an entity by replacing file and relation IDs with their respective objects. * - * @param array $entity The entity to render - * @param array|null $extend Optional array of properties to extend, defaults to files and relations if not provided + * Expands files and relations within the entity based on the provided extend array. * - * @return array The rendered entity with expanded files and relations - * @throws Exception + * @param array $entity The entity data to render. + * @param array|null $extend Optional properties to expand within the entity. + * + * @return array The rendered entity with expanded properties. + * @throws Exception If rendering or extending fails. */ - public function renderEntity(array $entity, ?array $extend = []): array - { + public function renderEntity(array $entity, ?array $extend = []): array + { // check if entity has files or relations and if not just return the entity if (array_key_exists(key: 'files', array: $entity) === false && array_key_exists(key: 'relations', array: $entity) === false) { return $entity; @@ -1136,17 +1184,19 @@ public function renderEntity(array $entity, ?array $extend = []): array return $this->extendEntity(entity: $entity, extend: $extend); } - /** - * Extends an entity with related objects based on the extend array. - * - * @param mixed $entity The entity to extend - * @param array $extend Properties to extend with related data + /** + * Extends an entity with related objects based on the provided properties. * - * @return array The extended entity as an array - * @throws Exception If property not found or no mapper available - */ - public function extendEntity(array $entity, array $extend): array - { + * Processes the extend array to replace IDs with full related objects within the entity. + * + * @param array $entity The entity data to extend. + * @param array $extend The list of properties to expand within the entity. + * + * @return array The extended entity with additional related data. + * @throws Exception If a property is missing or no mapper is available. + */ + public function extendEntity(array $entity, array $extend): array + { // Convert entity to array if needed if (is_array($entity)) { $result = $entity; @@ -1196,14 +1246,16 @@ public function extendEntity(array $entity, array $extend): array return $result; } - /** - * Get all registers extended with their schemas - * - * @return array The registers with schema data - * @throws Exception If extension fails - */ - public function getRegisters(): array - { + /** + * Retrieves all registers with their associated schema data. + * + * Converts registers to arrays and extends them with schema information as needed. + * + * @return array The list of registers with extended schema details. + * @throws Exception If extending schemas fails. + */ + public function getRegisters(): array + { // Get all registers $registers = $this->registerMapper->findAll(); @@ -1223,58 +1275,56 @@ public function getRegisters(): array return $registers; } - /** - * Get current register ID - * - * @return int The register ID - */ - public function getRegister(): int - { + /** + * Retrieves the current register ID. + * + * @return int The current register ID. + */ + public function getRegister(): int + { return $this->register; } - /** - * Set current register ID - * - * @param int $register The register ID to set - */ - public function setRegister(int $register): void - { + /** + * Sets the current register ID. + * + * @param int $register The register ID to set. + */ + public function setRegister(int $register): void + { $this->register = $register; } - /** - * Get current schema ID - * - * @return int The schema ID - */ - public function getSchema(): int - { + /** + * Retrieves the current schema ID. + * + * @return int The current schema ID. + */ + public function getSchema(): int + { return $this->schema; } - /** - * Set current schema ID - * - * @param int $schema The schema ID to set - */ - public function setSchema(int $schema): void - { + /** + * Sets the current schema ID. + * + * @param int $schema The schema ID to set. + */ + public function setSchema(int $schema): void + { $this->schema = $schema; } - /** - * Get the audit trail for a specific object - * - * @todo: register and schema parameters are not needed anymore - * - * @param int $register The register ID - * @param int $schema The schema ID - * @param string $id The object ID - * @return array The audit trail entries - */ - public function getAuditTrail(int $register, int $schema, string $id): array - { + /** + * Retrieves the audit trail for a specific object by its identifier. + * + * Processes audit trail entries to return a complete history for the specified object. + * @param string $id The unique identifier of the object. + * + * @return array The list of audit trail entries for the object. + */ + public function getAuditTrail(string $id): array + { $filters = [ 'object' => $id ]; From 75678f5715e8760933d9930aef0104ca446a5a6c Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 12 Dec 2024 16:54:15 +0100 Subject: [PATCH 09/21] added cascade delete in frontend --- src/modals/schema/EditSchemaProperty.vue | 25 +++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/modals/schema/EditSchemaProperty.vue b/src/modals/schema/EditSchemaProperty.vue index b46abb5..4648910 100644 --- a/src/modals/schema/EditSchemaProperty.vue +++ b/src/modals/schema/EditSchemaProperty.vue @@ -181,6 +181,13 @@ import { navigationStore, schemaStore } from '../../store/store.js' :loading="loading" :error="!verifyJsonValidity(properties.default)" :helper-text="!verifyJsonValidity(properties.default) ? 'This is not valid JSON' : ''" /> + + + Cascade delete + + - - Required - + + Required + + + + Cascade delete + Date: Thu, 12 Dec 2024 16:54:24 +0100 Subject: [PATCH 10/21] Added cascade delete --- lib/Db/ObjectEntityMapper.php | 23 ++++++++ lib/Service/ObjectService.php | 100 +++++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 860523d..9c064f0 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -91,6 +91,29 @@ public function findByUuid(Register $register, Schema $schema, string $uuid): Ob } } + /** + * Find an object by UUID only + * + * @param string $uuid The UUID of the object to find + * @return ObjectEntity The object + */ + public function findByUuidOnly(string $uuid): ObjectEntity|null + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_objects') + ->where( + $qb->expr()->eq('uuid', $qb->createNamedParameter($uuid)) + ); + + try { + return $this->findEntity($qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + } + /** * Find objects by register and schema * diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 6356c4b..63a1294 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -205,8 +205,8 @@ public function delete(array|\JsonSerializable $object): bool } return $this->deleteObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), + register: $this->getRegister(), + schema: $this->getSchema(), uuid: $object['id'] ); } @@ -762,21 +762,105 @@ public function getObject(Register $register, Schema $schema, string $uuid, ?arr throw new Exception('Unsupported source type'); } + /** + * Check if a string contains a dot and get the substring before the first dot. + * + * @param string $input The input string. + * + * @return string The substring before the first dot, or the original string if no dot is found. + */ + private function getStringBeforeDot(string $input): string + { + // Find the position of the first dot + $dotPosition = strpos($input, '.'); + + // Return the substring before the dot, or the original string if no dot is found + return $dotPosition !== false ? substr($input, 0, $dotPosition) : $input; + } + + /** + * Get the substring after the last slash in a string. + * + * @param string $input The input string. + * + * @return string The substring after the last slash. + */ + function getStringAfterLastSlash(string $input): string + { + // Find the position of the last slash + $lastSlashPos = strrpos($input, '/'); + + // Return the substring after the last slash, or the original string if no slash is found + return $lastSlashPos !== false ? substr($input, $lastSlashPos + 1) : $input; + } + + /** + * Cascade delete related objects based on schema properties. + * + * This method identifies properties in the schema marked for cascade deletion and deletes + * related objects associated with those properties in the given object. + * + * @param Register $register The register containing the objects. + * @param Schema $schema The schema defining the properties and relationships. + * @param ObjectEntity $object The object entity whose related objects should be deleted. + * + * @return void + * + * @throws Exception If any errors occur during the deletion process. + */ + private function cascadeDeleteObjects(Register $register, Schema $schema, ObjectEntity $object, string $originalObjectId): void + { + $cascadeDeleteProperties = []; + foreach ($schema->getProperties() as $propertyName => $property) { + if ((isset($property['cascadeDelete']) === true && $property['cascadeDelete'] === true) || (isset($property['items']['cascadeDelete']) === true && $property['items']['cascadeDelete'] === true)) { + $cascadeDeleteProperties[] = $propertyName; + } + } + + foreach ($object->getRelations() as $relationName => $relation) { + $relationName = $this->getStringBeforeDot(input: $relationName); + $relatedObjectId = $this->getStringAfterLastSlash(input: $relation); + // Check if this sub object has cacsadeDelete = true and is not the original object that started this delete streakt + if (in_array(needle: $relationName, haystack: $cascadeDeleteProperties) === true && $relatedObjectId !== $originalObjectId) { + $this->deleteObject(register: $register->getId(), schema: $schema->getId(), uuid: $relatedObjectId, originalObjectId: $originalObjectId); + } + } + } + /** * Delete an object * - * @param Register $register The register to delete from - * @param Schema $schema The schema of the object - * @param string $uuid The UUID of the object to delete + * @param string|int $register The register to delete from + * @param string|int $schema The schema of the object + * @param string $uuid The UUID of the object to delete + * @param string|null $originalObjectId The UUID of the parent object so we dont delete the object we come from and cause a loop * - * @return bool True if deletion was successful + * @return bool True if deletion was successful * @throws Exception If source type is unsupported */ - public function deleteObject(Register $register, Schema $schema, string $uuid): bool + public function deleteObject($register, $schema, string $uuid, ?string $originalObjectId = null): bool { + $register = $this->registerMapper->find($register); + $schema = $this->schemaMapper->find($schema); + // Handle internal source if ($register->getSource() === 'internal' || $register->getSource() === '') { - $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); + $object = $this->objectEntityMapper->findByUuidOnly(uuid: $uuid); + + if ($object === null) { + return false; + } + + // If internal register and schema should be found from the object himself. Makes it possible to delete cascaded objects. + $register = $this->registerMapper->find($object->getRegister()); + $schema = $this->schemaMapper->find($object->getSchema()); + + if ($originalObjectId === null) { + $originalObjectId = $object->getUuid(); + } + + $this->cascadeDeleteObjects(register: $register, schema: $schema, object: $object, originalObjectId: $originalObjectId); + $this->objectEntityMapper->delete($object); return true; } From addef18d8db820cc21c7f754b9518fd4946d7e29 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 16:55:45 +0100 Subject: [PATCH 11/21] Refactor on file synchronization --- lib/Db/File.php | 23 ++-- lib/Service/ObjectService.php | 234 +++++++++++++++++++++------------- 2 files changed, 161 insertions(+), 96 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index 18fdb7c..790ce6d 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -10,14 +10,16 @@ class File extends Entity implements JsonSerializable { protected string $uuid; - protected string $filename; - protected string $downloadUrl; - protected string $shareUrl; - protected string $accessUrl; - protected string $extension; - protected string $checksum; - protected string $source; - protected string $userId; + protected ?string $filename = null; + protected ?string $downloadUrl = null; + protected ?string $shareUrl = null; + protected ?string $accessUrl = null; + protected ?string $extension = null; + protected ?string $checksum = null; + protected ?string $source = null; + protected ?string $userId = null; + protected ?string $base64 = null; + protected ?string $filePath = null; protected DateTime $created; protected DateTime $updated; @@ -89,8 +91,6 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'type' => 'object', 'required' => [ - 'filename', - 'accessUrl', ], 'properties' => [ 'filename' => [ @@ -122,6 +122,9 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { ], 'userId' => [ 'type' => 'string', + ], + 'base64' => [ + 'type' => 'string' ] ] ]); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 86b883d..25f0b22 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -3,11 +3,13 @@ namespace OCA\OpenRegister\Service; use Adbar\Dot; +use DateTime; use Exception; use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use OC\URLGenerator; use OCA\OpenRegister\Db\File; +use OCA\OpenRegister\Db\FileMapper; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; @@ -76,6 +78,7 @@ public function __construct( private readonly FileService $fileService, private readonly IAppManager $appManager, private readonly IAppConfig $config, + private readonly FileMapper $fileMapper, ) { $this->objectEntityMapper = $objectEntityMapper; @@ -859,88 +862,10 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object return $objectEntity; } - /** - * Handle file property processing - * - * @param ObjectEntity $objectEntity The object entity - * @param array $object The object data - * @param string $propertyName The name of the file property - * - * @return array Updated object data - * @throws Exception|GuzzleException When file handling fails - */ - private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array - { - $fileName = str_replace('.', '_', $propertyName); - $objectDot = new Dot($object); - - // Handle base64 encoded file - if (is_string($objectDot->get($propertyName)) === true - && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches) - ) { - $fileContent = base64_decode($matches[2], true); - if ($fileContent === false) { - throw new Exception('Invalid base64 encoded file'); - } - } - // Handle URL file - else { - // Encode special characters in the URL - $encodedUrl = rawurlencode($objectDot->get("$propertyName.accessUrl")); //@todo hardcoded .downloadUrl - - // Decode valid path separators and reserved characters - $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); - - if (filter_var($encodedUrl, FILTER_VALIDATE_URL)) { - try { - // @todo hacky tacky - // Regular expression to get the filename and extension from url //@todo hardcoded .downloadUrl - if (preg_match("/\/([^\/]+)'\)\/\\\$value$/", $objectDot->get("$propertyName.downloadUrl"), $matches)) { - // @todo hardcoded way of getting the filename and extension from the url - $fileNameFromUrl = $matches[1]; - // @todo use only the extension from the url ? - // $fileName = $fileNameFromUrl; - $extension = substr(strrchr($fileNameFromUrl, '.'), 1); - $fileName = "$fileName.$extension"; - } - - if ($objectDot->has("$propertyName.source") === true) { - $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper'); - $source = $sourceMapper->find($objectDot->get("$propertyName.source")); - - $callService = $this->getOpenConnector(filePath: '\Service\CallService'); - if ($callService === null) { - throw new Exception("OpenConnector service not available"); - } - $endpoint = str_replace($source->getLocation(), "", $encodedUrl); - - $endpoint = urldecode($endpoint); - - $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); - - $fileContent = $response['body']; - - if( - $response['encoding'] === 'base64' - ) { - $fileContent = base64_decode(string: $fileContent); - } - - } else { - $client = new \GuzzleHttp\Client(); - $response = $client->get($encodedUrl); - $fileContent = $response->getBody()->getContents(); - } - } catch (Exception|NotFoundExceptionInterface $e) { - throw new Exception('Failed to download file from URL: ' . $e->getMessage()); - } - } else if (str_contains($objectDot->get($propertyName), $this->urlGenerator->getBaseUrl()) === true) { - return $object; - } else { - throw new Exception('Invalid file format - must be base64 encoded or valid URL'); - } - } + private function writeFile(string $fileContent, string $propertyName, ObjectEntity $objectEntity, File $file): File + { + $fileName = $file->getFileName(); try { $schema = $this->schemaMapper->find($objectEntity->getSchema()); @@ -964,9 +889,11 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s // Create or find ShareLink $share = $this->fileService->findShare(path: $filePath); if ($share !== null) { - $shareLink = $this->fileService->getShareLink($share).'/download'; + $shareLink = $this->fileService->getShareLink($share); + $downloadLink = $shareLink.'/download'; } else { - $shareLink = $this->fileService->createShareLink(path: $filePath).'/download'; + $shareLink = $this->fileService->createShareLink(path: $filePath); + $downloadLink = $shareLink.'/download'; } $filesDot = new Dot($objectEntity->getFiles() ?? []); @@ -974,13 +901,148 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $objectEntity->setFiles($filesDot->all()); // Preserve the original uri in the object 'json blob' - $objectDot = $objectDot->set($propertyName, $shareLink); - $object = $objectDot->all(); + $file->setDownloadUrl($downloadLink); + $file->setShareUrl($shareLink); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); } - return $object; + return $file; + } + + private function setExtension(File $file): File + { + // Regular expression to get the filename and extension from url + if ($file->getExtension() === false && preg_match("/\/([^\/]+)'\)\/\\\$value$/", $file->getAccessUrl(), $matches)) { + $fileNameFromUrl = $matches[1]; + $file->setExtension(substr(strrchr($fileNameFromUrl, '.'), 1)); + } + + return $file; + } + private function fetchFile(File $file, string $propertyName, ObjectEntity $objectEntity): File + { + $fileContent = null; + + // Encode special characters in the URL + $encodedUrl = rawurlencode($file->getAccessUrl()); + + + // Decode valid path separators and reserved characters + $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); + + if (filter_var($encodedUrl, FILTER_VALIDATE_URL)) { + $this->setExtension($file); + try { + + if ($file->getSource() !== null) { + $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper'); + $source = $sourceMapper->find($file->getSource()); + + $callService = $this->getOpenConnector(filePath: '\Service\CallService'); + if ($callService === null) { + throw new Exception("OpenConnector service not available"); + } + $endpoint = str_replace($source->getLocation(), "", $encodedUrl); + $endpoint = urldecode($endpoint); + $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); + + $fileContent = $response['body']; + + if( + $response['encoding'] === 'base64' + ) { + $fileContent = base64_decode(string: $fileContent); + } + + } else { + $client = new Client(); + $response = $client->get($encodedUrl); + $fileContent = $response->getBody()->getContents(); + } + } catch (Exception|NotFoundExceptionInterface $e) { + throw new Exception('Failed to download file from URL: ' . $e->getMessage()); + } + } + + $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $file); + + return $file; + } + + /** + * Handle file property processing + * + * @param ObjectEntity $objectEntity The object entity + * @param array $object The object data + * @param string $propertyName The name of the file property + * + * @return array Updated object data + * @throws Exception|GuzzleException When file handling fails + */ + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): array + { + $fileName = str_replace('.', '_', $propertyName); + $objectDot = new Dot($object); + + // Handle base64 encoded file + if (is_string($objectDot->get("$propertyName.base64")) === true + && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get("$propertyName.base64"), $matches) + ) { + unset($object[$propertyName]['base64']); + $fileEntity = new File(); + $fileEntity->hydrate($object[$propertyName]); + $fileEntity->setFilename($fileName); + $this->setExtension($fileEntity); + + $this->fileMapper->insert($fileEntity); + + $fileContent = base64_decode($matches[2], true); + if ($fileContent === false) { + throw new Exception('Invalid base64 encoded file'); + } + + $fileEntity = $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $fileEntity); + } + // Handle URL file + else { + $fileEntities = $this->fileMapper->findAll(filters: ['accessUrl' => $objectDot->get("$propertyName.accessUrl")]); + if(count($fileEntities) > 0) { + $fileEntity = $fileEntities[0]; + } + + if (count($fileEntities) === 0) { + $fileEntity = new File(); + $fileEntity->hydrate($object[$propertyName]); + } + + if($fileEntity->getFilename() === null) { + $fileEntity->setFilename($fileName); + } + + if($fileEntity->getChecksum() === null || $fileEntity->getUpdated() > new DateTime('-5 minutes')) { + $fileEntity = $this->fetchFile(file: $fileEntity, propertyName: $propertyName, objectEntity: $objectEntity); + $fileEntity->setUpdated(new DateTime()); + } + } + + $fileEntity->setChecksum(md5(serialize($fileContent))); + + $this->fileMapper->update($fileEntity); + + switch($format) { + case 'filename': + return $fileEntity->getFileName(); + case 'extension': + return $fileEntity->getExtension(); + case 'shareUrl': + return $fileEntity->getShareUrl(); + case 'accessUrl': + return $fileEntity->getAccessUrl(); + case 'downloadUrl': + default: + return $fileEntity->getDownloadUrl(); + } } /** From 6957ef5bcab004bf54caf99ebe516f95baca87a9 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Mon, 16 Dec 2024 16:51:52 +0100 Subject: [PATCH 12/21] Fixes from testing --- lib/Db/File.php | 8 +-- lib/Db/FileMapper.php | 45 +++++++++++-- lib/Migration/Version1Date20241216094112.php | 70 ++++++++++++++++++++ lib/Service/ObjectService.php | 17 +++-- 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 lib/Migration/Version1Date20241216094112.php diff --git a/lib/Db/File.php b/lib/Db/File.php index 790ce6d..c4c946d 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -9,19 +9,19 @@ class File extends Entity implements JsonSerializable { - protected string $uuid; + protected ?string $uuid = null; protected ?string $filename = null; protected ?string $downloadUrl = null; protected ?string $shareUrl = null; protected ?string $accessUrl = null; protected ?string $extension = null; protected ?string $checksum = null; - protected ?string $source = null; + protected ?int $source = null; protected ?string $userId = null; protected ?string $base64 = null; protected ?string $filePath = null; - protected DateTime $created; - protected DateTime $updated; + protected ?DateTime $created = null; + protected ?DateTime $updated = null; public function __construct() { $this->addType('uuid', 'string'); diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index e3e29ff..e495d85 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -2,6 +2,7 @@ namespace OCA\OpenRegister\Db; +use DateTime; use OCA\OpenRegister\Db\File; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; @@ -13,7 +14,7 @@ class FileMapper extends QBMapper { public function __construct(IDBConnection $db) { - parent::__construct($db, 'openconnector_jobs'); + parent::__construct($db, 'openregister_files'); } public function find(int $id): File @@ -21,7 +22,7 @@ public function find(int $id): File $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('openconnector_jobs') + ->from('openregister_files') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); @@ -34,11 +35,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('openconnector_jobs') + ->from('openregister_files') ->setMaxResults($limit) ->setFirstResult($offset); foreach ($filters as $filter => $value) { + $filter = strtolower(preg_replace('/(?andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { @@ -58,6 +60,41 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * @inheritDoc + * + * @param \OCA\OpenRegister\Db\File|Entity $entity + * @return \OCA\OpenRegister\Db\File + * @throws \OCP\DB\Exception + */ + public function insert(File|Entity $entity): File + { + // Set created and updated fields + $entity->setCreated(new DateTime()); + $entity->setUpdated(new DateTime()); + + if($entity->getUuid() === null) { + $entity->setUuid(Uuid::v4()); + } + + return parent::insert($entity); + } + + /** + * @inheritDoc + * + * @param \OCA\OpenRegister\Db\File|Entity $entity + * @return \OCA\OpenRegister\Db\File + * @throws \OCP\DB\Exception + */ + public function update(File|Entity $entity): File + { + // Set updated field + $entity->setUpdated(new DateTime()); + + return parent::update($entity); + } + public function createFromArray(array $object): File { $obj = new File(); @@ -93,7 +130,7 @@ public function getTotalCallCount(): int // Select count of all logs $qb->select($qb->createFunction('COUNT(*) as count')) - ->from('openconnector_jobs'); + ->from('openregister_files'); $result = $qb->execute(); $row = $result->fetch(); diff --git a/lib/Migration/Version1Date20241216094112.php b/lib/Migration/Version1Date20241216094112.php new file mode 100644 index 0000000..341dc4b --- /dev/null +++ b/lib/Migration/Version1Date20241216094112.php @@ -0,0 +1,70 @@ +hasTable('openregister_files') === false) { + $table = $schema->createTable('openregister_files'); + $table->addColumn(name: 'id', typeName: Types::BIGINT, options: ['autoincrement' => true, 'notnull' => true, 'length' => 255]); + $table->addColumn(name: 'uuid', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'filename', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'download_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'share_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'access_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'extension', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'checksum', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'source', typeName: Types::INTEGER, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'user_id', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'created', typeName: Types::DATETIME_IMMUTABLE, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'updated', typeName: Types::DATETIME_MUTABLE, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'file_path', typeName: Types::STRING); + + $table->setPrimaryKey(['id']); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 25f0b22..f73b8b6 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -865,7 +865,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object private function writeFile(string $fileContent, string $propertyName, ObjectEntity $objectEntity, File $file): File { - $fileName = $file->getFileName(); + $fileName = $file->getFilename(); try { $schema = $this->schemaMapper->find($objectEntity->getSchema()); @@ -875,7 +875,13 @@ private function writeFile(string $fileContent, string $propertyName, ObjectEnti $this->fileService->createFolder(folderPath: 'Objects'); $this->fileService->createFolder(folderPath: "Objects/$schemaFolder"); $this->fileService->createFolder(folderPath: "Objects/$schemaFolder/$objectFolder"); - $filePath = "Objects/$schemaFolder/$objectFolder/$fileName"; + + $filePath = $file->getFilePath(); + + if($filePath === null) { + $filePath = "Objects/$schemaFolder/$objectFolder/$fileName"; + } + $succes = $this->fileService->updateFile( content: $fileContent, @@ -903,6 +909,7 @@ private function writeFile(string $fileContent, string $propertyName, ObjectEnti // Preserve the original uri in the object 'json blob' $file->setDownloadUrl($downloadLink); $file->setShareUrl($shareLink); + $file->setFilePath($filePath); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); } @@ -965,6 +972,7 @@ private function fetchFile(File $file, string $propertyName, ObjectEntity $objec } } + $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $file); return $file; @@ -980,7 +988,7 @@ private function fetchFile(File $file, string $propertyName, ObjectEntity $objec * @return array Updated object data * @throws Exception|GuzzleException When file handling fails */ - private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): array + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): string { $fileName = str_replace('.', '_', $propertyName); $objectDot = new Dot($object); @@ -1012,8 +1020,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } if (count($fileEntities) === 0) { - $fileEntity = new File(); - $fileEntity->hydrate($object[$propertyName]); + $fileEntity = $this->fileMapper->createFromArray($object[$propertyName]); } if($fileEntity->getFilename() === null) { From d6930f2a4e5015d71149774c5eb1bd027fe433c4 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Wed, 18 Dec 2024 13:30:33 +0100 Subject: [PATCH 13/21] add oneOf in frontend --- lib/Service/ObjectService.php | 10 ++- src/modals/schema/EditSchemaProperty.vue | 77 +++++++++++++++++++----- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 540f171..58e31ad 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -152,6 +152,12 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $schemaObject = $this->schemaMapper->find($schemaId)->getSchemaObject($this->urlGenerator); } + // if there are no properties we dont have to validate + if ($schemaObject instanceof stdClass || !method_exists($schemaObject, 'getProperties')) { + // Return a default ValidationResult indicating success + return new ValidationResult(null); + } + $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); @@ -1048,7 +1054,7 @@ private function getStringBeforeDot(string $input): string { // Find the position of the first dot $dotPosition = strpos($input, '.'); - + // Return the substring before the dot, or the original string if no dot is found return $dotPosition !== false ? substr($input, 0, $dotPosition) : $input; } @@ -1064,7 +1070,7 @@ function getStringAfterLastSlash(string $input): string { // Find the position of the last slash $lastSlashPos = strrpos($input, '/'); - + // Return the substring after the last slash, or the original string if no slash is found return $lastSlashPos !== false ? substr($input, $lastSlashPos + 1) : $input; } diff --git a/src/modals/schema/EditSchemaProperty.vue b/src/modals/schema/EditSchemaProperty.vue index 4648910..2cdf7f1 100644 --- a/src/modals/schema/EditSchemaProperty.vue +++ b/src/modals/schema/EditSchemaProperty.vue @@ -34,7 +34,8 @@ import { navigationStore, schemaStore } from '../../store/store.js' - @@ -174,19 +175,21 @@ import { navigationStore, schemaStore } from '../../store/store.js' :value.sync="properties.default" :loading="loading" /> - +
+ - - Cascade delete + + Cascade delete +
- @@ -292,6 +294,41 @@ import { navigationStore, schemaStore } from '../../store/store.js' label="Maximum number of items" :value.sync="properties.maxItems" /> + + +
+
type: oneOf
+ +
+
oneOf entry {{ index + 1 }}
+ +
+ +
+ +
+ +
+ + + Remove oneOf entry + +
+ + + Add oneOf entry + +