diff --git a/lib/Db/File.php b/lib/Db/File.php
new file mode 100644
index 0000000000..3214645fa8
--- /dev/null
+++ b/lib/Db/File.php
@@ -0,0 +1,217 @@
+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('base64', 'string');
+ $this->addType('filePath', 'string');
+ $this->addType('created', 'datetime');
+ $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(
+ array_filter($this->getFieldTypes(), function ($field) {
+ return $field === 'json';
+ })
+ );
+ }
+
+ /**
+ * 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();
+
+ foreach ($object as $key => $value) {
+ if (in_array($key, $jsonFields) === true && $value === []) {
+ $value = [];
+ }
+
+ $method = 'set' . ucfirst($key);
+
+ try {
+ $this->$method($value);
+ } 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 [
+ '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,
+ 'base64' => $this->base64,
+ 'filePath' => $this->filePath,
+ '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',
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
+ 'type' => 'object',
+ 'required' => [
+ ],
+ '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',
+ ],
+ 'base64' => [
+ 'type' => 'string'
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php
new file mode 100644
index 0000000000..59bf430bdf
--- /dev/null
+++ b/lib/Db/FileMapper.php
@@ -0,0 +1,191 @@
+db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from('openregister_files')
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
+ );
+
+ 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();
+
+ $qb->select('*')
+ ->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') {
+ $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);
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * 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) {
+ $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);
+ $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);
+ }
+
+ /**
+ * Gets the total count of files.
+ *
+ * @return int The total number of files in the database.
+ * @throws Exception If a database error occurs.
+ */
+ public function countAll(): int
+ {
+ $qb = $this->db->getQueryBuilder();
+
+ // Select count of all files
+ $qb->select($qb->createFunction('COUNT(*) as count'))
+ ->from('openregister_files');
+
+ $result = $qb->execute();
+ $row = $result->fetch();
+
+ // Return the total count
+ return (int)$row['count'];
+ }
+}
diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php
index f31fdafdf8..387fb6ff24 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 5d7507928a..1b3f0a8933 100644
--- a/lib/Db/ObjectEntityMapper.php
+++ b/lib/Db/ObjectEntityMapper.php
@@ -92,6 +92,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
*
@@ -246,7 +269,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 569d8f5e31..1f573d8fff 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 571618ce66..db08bf7745 100644
--- a/lib/Db/Schema.php
+++ b/lib/Db/Schema.php
@@ -132,6 +132,28 @@ 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']);
+ }
+ 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'],
@@ -141,7 +163,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/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php
index 35b22c3447..11f3953405 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 02d0175bfd..7151106573 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/Migration/Version1Date20241216094112.php b/lib/Migration/Version1Date20241216094112.php
new file mode 100644
index 0000000000..f899f7ab16
--- /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)->setNotnull(false)->setDefault(null);
+
+ $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/FileService.php b/lib/Service/FileService.php
index 9d547b559b..2d9a8326ff 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 3331ef1eae..43074209b6 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 32a6190cb0..0ec366366a 100755
--- a/lib/Service/ObjectService.php
+++ b/lib/Service/ObjectService.php
@@ -3,10 +3,13 @@
namespace OCA\OpenRegister\Service;
use Adbar\Dot;
+use DateTime;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
-use OC\URLGenerator;
+use JsonSerializable;
+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;
@@ -15,15 +18,16 @@
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\Db\ObjectAuditLogMapper;
use OCA\OpenRegister\Exception\ValidationException;
use OCA\OpenRegister\Formats\BsnFormat;
use OCP\App\IAppManager;
+use OCP\IAppConfig;
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;
@@ -32,70 +36,70 @@
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;
- /** @var AuditTrailMapper For tracking object changes */
- private AuditTrailMapper $auditTrailMapper;
-
- /**
- * 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,
- ObjectAuditLogMapper $objectAuditLogMapper,
- private ContainerInterface $container,
- private readonly IURLGenerator $urlGenerator,
- private readonly FileService $fileService,
- private readonly IAppManager $appManager
- )
- {
- $this->objectEntityMapper = $objectEntityMapper;
- $this->registerMapper = $registerMapper;
- $this->schemaMapper = $schemaMapper;
- $this->auditTrailMapper = $auditTrailMapper;
- $this->objectAuditLogMapper = $objectAuditLogMapper;
- }
+ /**
+ * 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,
+ private readonly RegisterMapper $registerMapper,
+ private readonly SchemaMapper $schemaMapper,
+ private readonly AuditTrailMapper $auditTrailMapper,
+ private readonly ObjectAuditLogMapper $objectAuditLogMapper,
+ private readonly ContainerInterface $container,
+ private readonly IURLGenerator $urlGenerator,
+ private readonly FileService $fileService,
+ private readonly IAppManager $appManager,
+ private readonly IAppConfig $config,
+ private readonly FileMapper $fileMapper,
+ )
+ {
+ }
/**
- * Attempts to retrieve the OpenConnector service from the container.
+ * Retrieves the OpenConnector service from the container.
+ *
+ * @param string $filePath Optional file path for the OpenConnector service.
*
- * @return mixed|null The OpenConnector service if available, null otherwise.
- * @throws ContainerExceptionInterface|NotFoundExceptionInterface
+ * @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;
}
}
@@ -104,14 +108,51 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m
}
/**
- * Validate an object with a schema.
- * If schema is not given and schemaObject is filled, the object will validate to the schemaObject.
+ * Resolves a schema from a given URI.
+ *
+ * @param Uri $uri The URI pointing to the schema.
+ *
+ * @return string The schema content in JSON format.
+ * @throws GuzzleException If there is an error during schema fetching.
+ */
+ public function resolveSchema(Uri $uri): string
+ {
+ // Local schema resolution
+ if ($this->urlGenerator->getBaseUrl() === $uri->scheme() . '://' . $uri->host()
+ && str_contains($uri->path(), '/api/schemas')
+ ) {
+ $exploded = explode('/', $uri->path());
+ $schema = $this->schemaMapper->find(end($exploded));
+
+ return json_encode($schema->getSchemaObject($this->urlGenerator));
+ }
+
+ // File schema resolution
+ if ($this->urlGenerator->getBaseUrl() === $uri->scheme() . '://' . $uri->host()
+ && str_contains($uri->path(), '/api/files/schema')
+ ) {
+ return File::getSchema($this->urlGenerator);
+ }
+
+ // External schema resolution
+ if ($this->config->getValueBool('openregister', 'allowExternalSchemas')) {
+ $client = new Client();
+ $result = $client->get(\GuzzleHttp\Psr7\Uri::fromParts($uri->components()));
+
+ return $result->getBody()->getContents();
+ }
+
+ return '';
+ }
+
+ /**
+ * 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
{
@@ -119,119 +160,129 @@ 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());
+ $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|null The found object or null if not found
- * @throws Exception
+ * @throws Exception If the object is not found.
*/
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
+ * @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
+ * @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->getRegister(),
+ schema: $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,
+ ?int $limit = null,
+ ?int $offset = null,
+ array $filters = [],
+ array $sort = [],
+ ?string $search = null,
?array $extend = []
): array
{
@@ -257,42 +308,43 @@ public function findAll(
return $objects;
}
- /**
- * Count total objects matching 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
- {
- // Add register and schema filters if set
- if ($this->getSchema() !== null && $this->getRegister() !== null) {
- $filters['register'] = $this->getRegister();
- $filters['schema'] = $this->getSchema();
- }
+ /**
+ * Counts the total number of objects matching criteria.
+ *
+ * @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;
+ }
/**
* Find subobjects for a certain property with given ids
@@ -325,36 +377,37 @@ public function findSubObjects(array $ids, string $property): array
* @param array $filters Filter criteria
* @param string|null $search Search term
*
- * @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();
+ }
/**
* Find all objects conforming to the request parameters, surrounded with pagination data.
@@ -413,26 +466,27 @@ public function findAllPaginated(array $requestParams): array
/**
* 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
+ * @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 The retrieved objects.
+ * @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 $objectType = null,
+ ?int $register = null,
+ ?int $schema = null,
+ ?int $limit = null,
+ ?int $offset = null,
+ array $filters = [],
+ array $sort = [],
?string $search = null
)
{
@@ -443,29 +497,28 @@ public function getObjects(
$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,
+ 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
+ * @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
{
@@ -479,77 +532,82 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt
$register = $this->registerMapper->find($register);
}
- if (is_string($schema)) {
- $schema = $this->schemaMapper->find($schema);
- }
+ if (is_string($schema)) {
+ $schema = $this->schemaMapper->find($schema);
+ }
- // Check if object already exists
- if (isset($object['id']) === true) {
- $objectEntity = $this->objectEntityMapper->findByUuid(
- $this->registerMapper->find($register),
- $this->schemaMapper->find($schema),
- $object['id']
- );
- }
+ // Check if object already exists
+ if (isset($object['id']) === true) {
+ $objectEntity = $this->objectEntityMapper->findByUuid(
+ $this->registerMapper->find($register),
+ $this->schemaMapper->find($schema),
+ $object['id']
+ );
+ }
- // Create new entity if none exists
- if (isset($object['id']) === false || $objectEntity === null) {
- $objectEntity = new ObjectEntity();
- $objectEntity->setRegister($register);
- $objectEntity->setSchema($schema);
- }
+ $validationResult = $this->validateObject(object: $object, schemaId: $schema);
- // Handle UUID assignment
- if (isset($object['id']) && !empty($object['id'])) {
- $objectEntity->setUuid($object['id']);
- } else {
- $objectEntity->setUuid(Uuid::v4());
- $object['id'] = $objectEntity->getUuid();
- }
+ // Create new entity if none exists
+ if (isset($object['id']) === false || $objectEntity === null) {
+ $objectEntity = new ObjectEntity();
+ $objectEntity->setRegister($register);
+ $objectEntity->setSchema($schema);
+ }
- // Store old version for audit trail
- $oldObject = clone $objectEntity;
- $objectEntity->setObject($object);
+ // Handle UUID assignment
+ if (isset($object['id']) && !empty($object['id'])) {
+ $objectEntity->setUuid($object['id']);
+ } else {
+ $objectEntity->setUuid(Uuid::v4());
+ $object['id'] = $objectEntity->getUuid();
+ }
- // Ensure UUID exists //@todo: this is not needed anymore? this kinde of uuid is set in the handleLinkRelations function
- if (empty($objectEntity->getUuid()) === true) {
- $objectEntity->setUuid(Uuid::v4());
- }
+ // Store old version for audit trail
+ $oldObject = clone $objectEntity;
+ $objectEntity->setObject($object);
- // Let grap any links that we can
- $objectEntity = $this->handleLinkRelations($objectEntity, $object);
+ // Ensure UUID exists //@todo: this is not needed anymore? this kinde of uuid is set in the handleLinkRelations function
+ if (empty($objectEntity->getUuid()) === true) {
+ $objectEntity->setUuid(Uuid::v4());
+ }
+
+ // Let grap any links that we can
+ $objectEntity = $this->handleLinkRelations($objectEntity, $object);
$schemaObject = $this->schemaMapper->find($schema);
- // Handle object properties that are either nested objects or files
+ // 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->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()])));
- if ($objectEntity->getId()) {
- $objectEntity = $this->objectEntityMapper->update($objectEntity);
- $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject);
- } else {
- $objectEntity = $this->objectEntityMapper->insert($objectEntity);
- $this->auditTrailMapper->createAuditTrail(new: $objectEntity);
- }
+ 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) {
+ $objectEntity = $this->objectEntityMapper->insert($objectEntity);
+ $this->auditTrailMapper->createAuditTrail(new: $objectEntity);
+ }
- return $objectEntity;
- }
+ if ($validationResult->isValid() === false) {
+ throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error());
+ }
+
+ return $objectEntity;
+ }
/**
- * Handle link relations efficiently using JSON path traversal
- *
- * Finds all links or UUIDs in the object and adds them to the relations
- * using dot notation paths for nested properties
- *
- * @param ObjectEntity $objectEntity The object entity to handle relations for
- * @param array $object The object data
- *
- * @return ObjectEntity Updated object data
- */
+ * Efficiently processes link relations within an object using JSON path traversal.
+ *
+ * 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 analyze and update relations for.
+ *
+ * @return ObjectEntity The updated object entity with new relations mapped.
+ */
private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity
{
$relations = $objectEntity->getRelations() ?? [];
@@ -562,7 +620,7 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity
];
// Function to recursively find links/UUIDs and build dot notation paths
- $findRelations = function($data, $path = '') use (&$findRelations, &$relations, $selfIdentifiers) {
+ $findRelations = function ($data, $path = '') use (&$findRelations, &$relations, $selfIdentifiers) {
foreach ($data as $key => $value) {
$currentPath = $path ? "$path.$key" : $key;
@@ -572,7 +630,7 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity
} else if (is_string($value) === true) {
// Check for URLs and UUIDs
if ((filter_var($value, FILTER_VALIDATE_URL) !== false
- || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1)
+ || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1)
&& in_array($value, $selfIdentifiers, true) === false
) {
$relations[$currentPath] = $value;
@@ -589,224 +647,384 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity
}
/**
- * Handle object relations and file properties in schema properties and array items
+ * Adds a nested subobject based on schema and property details and incorporates it into the main object.
*
- * @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
+ * Handles $ref resolution for schema subtypes, stores relations, and replaces nested subobject
+ * data with its reference URI or UUID.
*
- * @return ObjectEntity Updated object with linked data
- * @throws Exception|ValidationException When file handling fails
+ * @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 handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): ObjectEntity
+ private function addObject(
+ array $property,
+ string $propertyName,
+ array $item,
+ ObjectEntity $objectEntity,
+ int $register,
+ int $schema,
+ ?int $index = null
+ ): string
{
- // @todo: Multidimensional suport should be added
- foreach ($properties as $propertyName => $property) {
- // Skip if property not in object
- if (isset($object[$propertyName]) === false) {
- 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);
- }
+ $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);
+ }
- } 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 nested object in array
+ $nestedObject = $this->saveObject(
+ register: $register,
+ schema: $subSchema,
+ object: $item
+ );
- // 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 ($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);
+ }
- if(is_array($object[$propertyName]) === true) {
- $nestedObject = $this->saveObject(
- register: $register,
- schema: $subSchema,
- object: $object[$propertyName]
- );
+ return $nestedObject->getUuid();
+ }
- // Store relation and replace with reference
- $relations = $objectEntity->getRelations() ?? [];
- $relations[$propertyName] = $nestedObject->getUri();
- $objectEntity->setRelations($relations);
- $object[$propertyName] = $nestedObject->getUri();
+ /**
+ * Processes an object property by delegating it to a subobject handling mechanism.
+ *
+ * @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 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,
+ ObjectEntity $objectEntity,
+ int $register,
+ int $schema
+ ): string
+ {
+ return $this->addObject(
+ property: $property,
+ propertyName: $propertyName,
+ item: $item,
+ objectEntity: $objectEntity,
+ register: $register,
+ schema: $schema
+ );
+ }
- } else {
- $relations = $objectEntity->getRelations() ?? [];
- $relations[$propertyName] = $object[$propertyName];
- $objectEntity->setRelations($relations);
- }
+ /**
+ * Handles array-type properties by processing each element based on its schema type.
+ *
+ * Supports nested objects, files, or oneOf schema types, delegating to specific handlers
+ * for each element in the array.
+ *
+ * @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,
+ ObjectEntity $objectEntity,
+ int $register,
+ int $schema
+ ): array
+ {
+ if (isset($property['items']) === false) {
+ return $items;
+ }
+ if (isset($property['items']['oneOf']) === true) {
+ foreach ($items as $index => $item) {
+ $items[$index] = $this->handleOneOfProperty(
+ property: $property['items']['oneOf'],
+ propertyName: $propertyName,
+ item: $item,
+ objectEntity: $objectEntity,
+ register: $register,
+ schema: $schema,
+ index: $index
+ );
}
- // Handle single file type
- else if ($property['type'] === 'file') {
+ return $items;
+ }
+
+ if ($property['items']['type'] !== 'object'
+ && $property['items']['type'] !== 'file'
+ ) {
+ return $items;
+ }
- $object = $this->handleFileProperty(
+ if ($property['items']['type'] === 'file') {
+ foreach ($items as $index => $item) {
+ $items[$index] = $this->handleFileProperty(
objectEntity: $objectEntity,
- object: $object,
- propertyName: $propertyName
- );
+ object: [$propertyName => [$index => $item]],
+ propertyName: $propertyName . '.' . $index,
+ format: $item['format'] ?? null
+ )[$propertyName];
}
+ return $items;
}
- $objectEntity->setObject($object);
+ 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 $objectEntity;
+ return $items;
}
/**
- * Handle file property processing
+ * Processes properties defined as oneOf, selecting the appropriate schema option for the data.
*
- * @param ObjectEntity $objectEntity The object entity
- * @param array $object The object data
- * @param string $propertyName The name of the file property
+ * Handles various types of schemas, including files and references, to correctly process
+ * and replace the input data with the resolved references or processed results.
*
- * @return array Updated object data
- * @throws Exception|GuzzleException When file handling fails
+ * @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 handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array
+ private function handleOneOfProperty(
+ array $property,
+ string $propertyName,
+ string|array $item,
+ ObjectEntity $objectEntity,
+ int $register,
+ int $schema,
+ ?int $index = null
+ ): string|array
{
- $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;
-// }
-// }
+ if (array_is_list($property) === false) {
+ return $item;
+ }
- // Handle base64 encoded file
- if (is_string($objectDot->get($propertyName)) === true
- && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches)
+ if (in_array(needle:'file', haystack: array_column(array: $property, column_key: 'type')) === true
+ && is_array($item) === true
+ && $index !== null
) {
- $fileContent = base64_decode($matches[2], true);
- if ($fileContent === false) {
- throw new Exception('Invalid base64 encoded file');
- }
+ $fileIndex = array_search(needle: 'file', haystack: array_column(array: $property, column_key: 'type'));
+ return $this->handleFileProperty(
+ objectEntity: $objectEntity,
+ object: [$propertyName => [$index => $item]],
+ propertyName: $propertyName,
+ format: $property[$fileIndex]['format'] ?? null
+ );
+ }
+ if (in_array(needle: 'file', haystack: array_column(array: $property, column_key: 'type')) === true
+ && is_array($item) === true
+ && $index === null
+ ) {
+ $fileIndex = array_search(needle: 'file', haystack: array_column(array: $property, column_key: 'type'));
+ return $this->handleFileProperty(
+ objectEntity: $objectEntity,
+ object: [$propertyName => $item],
+ propertyName: $propertyName,
+ format: $property[$fileIndex]['format'] ?? null
+ );
}
- // Handle URL file
- else {
- // Encode special characters in the URL
- $encodedUrl = rawurlencode($objectDot->get("$propertyName.downloadUrl")); //@todo hardcoded .downloadUrl
- // Decode valid path separators and reserved characters
- $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl);
+ if (array_column(array: $property, column_key: '$ref') === []) {
+ return $item;
+ }
- 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 (is_array($item) === false) {
+ return $item;
+ }
- if ($objectDot->has("$propertyName.source") === true) {
- $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper');
- $source = $sourceMapper->find($objectDot->get("$propertyName.source"));
+ $oneOf = array_filter(
+ array: $property,
+ callback: function (array $option) {
+ return isset($option['$ref']) === true;
+ }
+ )[0];
+
+ return $this->addObject(
+ property: $oneOf,
+ propertyName: $propertyName,
+ item: $item,
+ objectEntity: $objectEntity,
+ register: $register,
+ schema: $schema,
+ index: $index
+ );
+ }
- $callService = $this->getOpenConnector(filePath: '\Service\CallService');
- if ($callService === null) {
- throw new Exception("OpenConnector service not available");
- }
- $endpoint = str_replace($source->getLocation(), "", $encodedUrl);
+ /**
+ * 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(
+ array $property,
+ string $propertyName,
+ int $register,
+ int $schema,
+ array $object,
+ ObjectEntity $objectEntity
+ ): array
+ {
+ 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['oneOf'],
+ propertyName: $propertyName,
+ item: $object[$propertyName],
+ objectEntity: $objectEntity,
+ register: $register,
+ schema: $schema);
+ break;
+ case 'file':
+ $object[$propertyName] = $this->handleFileProperty(
+ objectEntity: $objectEntity,
+ object: $object,
+ propertyName: $propertyName,
+ format: $property['format'] ?? null
+ );
+ default:
+ break;
+ }
+ return $object;
+ }
- $endpoint = urldecode($endpoint);
- $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse();
+ /**
+ * Links object relations and handles file-based properties within an object schema.
+ *
+ * Iterates through schema-defined properties, processing and resolving nested relations,
+ * array items, and file-based data. Updates the object entity with resolved references.
+ *
+ * @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
+ {
+ // @todo: Multidimensional support should be added
+ foreach ($properties as $propertyName => $property) {
+ // Skip if property not in object
+ if (isset($object[$propertyName]) === false) {
+ continue;
+ }
+
+ $object = $this->handleProperty(
+ property: $property,
+ propertyName: $propertyName,
+ register: $register,
+ schema: $schema,
+ object: $object,
+ objectEntity: $objectEntity,
+ );
+ }
- $fileContent = $response['body'];
+ $objectEntity->setObject($object);
- if(
- $response['encoding'] === 'base64'
- ) {
- $fileContent = base64_decode(string: $fileContent);
- }
+ return $objectEntity;
+ }
- } 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');
- }
- }
+ /**
+ * @todo
+ *
+ * @param string $fileContent
+ * @param string $propertyName
+ * @param ObjectEntity $objectEntity
+ * @param File $file
+ *
+ * @return File
+ * @throws Exception
+ */
+ private function writeFile(string $fileContent, string $propertyName, ObjectEntity $objectEntity, File $file): File
+ {
+ $fileName = $file->getFilename();
try {
$schema = $this->schemaMapper->find($objectEntity->getSchema());
@@ -816,13 +1034,19 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s
$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,
filePath: $filePath,
createNew: true
);
+
if ($succes === false) {
throw new Exception('Failed to upload this file: $filePath to NextCloud');
}
@@ -830,9 +1054,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() ?? []);
@@ -840,394 +1066,648 @@ 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);
+ $file->setFilePath($filePath);
} catch (Exception $e) {
throw new Exception('Failed to store file: ' . $e->getMessage());
}
- return $object;
+ return $file;
}
- /**
- * 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|null The resulting object or null if not found
- * @throws Exception If source type is unsupported
- */
- public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ?ObjectEntity
- {
- // Handle internal source
- if ($register->getSource() === 'internal' || $register->getSource() === '') {
- return $this->objectEntityMapper->findByUuid($register, $schema, $uuid);
- }
+ /**
+ * @todo
+ *
+ * @param File $file
+ *
+ * @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));
+ }
- //@todo mongodb support
+ return $file;
+ }
- throw new Exception('Unsupported source type');
- }
+ /**
+ * @todo
+ *
+ * @param File $file
+ * @param string $propertyName
+ * @param ObjectEntity $objectEntity
+ *
+ * @return File
+ * @throws ContainerExceptionInterface
+ * @throws GuzzleException
+ */
+ private function fetchFile(File $file, string $propertyName, ObjectEntity $objectEntity): File
+ {
+ $fileContent = null;
- /**
- * 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
- {
- // Handle internal source
- if ($register->getSource() === 'internal' || $register->getSource() === '') {
- $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid);
- $this->objectEntityMapper->delete($object);
- return true;
- }
+ // Encode special characters in the URL
+ $encodedUrl = rawurlencode($file->getAccessUrl());
- //@todo mongodb support
+ // Decode valid path separators and reserved characters
+ $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl);
- throw new Exception('Unsupported source type');
- }
+ 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());
- /**
- * 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
- {
- // Return self if register and schema provided
- if ($register !== null && $schema !== null) {
- $this->setSchema($schema);
- $this->setRegister($register);
- return $this;
- }
+ $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();
- // Return appropriate mapper based on object type
- switch ($objectType) {
- case 'register':
- return $this->registerMapper;
- case 'schema':
- return $this->schemaMapper;
- case 'objectEntity':
- return $this->objectEntityMapper;
- default:
- throw new InvalidArgumentException("Unknown object type: $objectType");
- }
- }
+ $fileContent = $response['body'];
- /**
- * 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
- * @return array The retrieved objects
- * @throws InvalidArgumentException If unknown object type
- */
- public function getMultipleObjects(string $objectType, array $ids): array
+ 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;
+ }
+
+ /**
+ * 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 containing the file property.
+ * @param array $object The parent object data containing the file reference.
+ * @param string $propertyName The name of the file property.
+ * @param string|null $format
+ *
+ * @return string The updated object with resolved file references.
+ * @throws ContainerExceptionInterface
+ * @throws GuzzleException When file handling fails
+ * @throws \OCP\DB\Exception
+ */
+ private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): string
{
- // Process the ids to handle different formats
- $processedIds = array_map(function($id) {
- if (is_object($id) && method_exists($id, 'getId')) {
- return $id->getId();
- } elseif (is_array($id) && isset($id['id'])) {
- return $id['id'];
- } else {
- return $id;
- }
- }, $ids);
-
- // Clean up URIs to get just the ID portion
- $cleanedIds = array_map(function($id) {
- if (filter_var($id, FILTER_VALIDATE_URL)) {
- $parts = explode('/', rtrim($id, '/'));
- return end($parts);
- }
- return $id;
- }, $processedIds);
-
- // Get mapper and find objects
- $mapper = $this->getMapper($objectType);
- return $mapper->findMultiple($cleanedIds);
- }
+ $fileName = str_replace('.', '_', $propertyName);
+ $objectDot = new Dot($object);
- /**
- * 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
- */
- 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;
- }
+ // 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');
+ }
- // Lets create a dot array of the entity
- $dotEntity = new Dot($entity);
+ $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];
+ }
- // loop through the files and replace the file ids with the file objects)
- if (array_key_exists(key: 'files', array: $entity) === true && empty($entity['files']) === false) {
- // Loop through the files array where key is dot notation path and value is file id
- foreach ($entity['files'] as $path => $fileId) {
- // Replace the value at the dot notation path with the file URL
- $dotEntity->set($path, $filesById[$fileId]->getUrl());
- }
- }
+ if (count($fileEntities) === 0) {
+ $fileEntity = $this->fileMapper->createFromArray($object[$propertyName]);
+ }
- // Loop through the relations and replace the relation ids with the relation objects if extended
- if (array_key_exists(key: 'relations', array: $entity) === true && empty($entity['relations']) === false) {
- // loop through the relations and replace the relation ids with the relation objects
- foreach ($entity['relations'] as $path => $relationId) {
- // if the relation is not in the extend array, skip it
- if (in_array(needle: $path, haystack: $extend) === false) {
- continue;
- }
- // Replace the value at the dot notation path with the relation object
- $dotEntity->set($path, $this->getObject(register: $this->getRegister(), schema: $this->getSchema(), uuid: $relationId));
- }
- }
+ if ($fileEntity->getFilename() === null) {
+ $fileEntity->setFilename($fileName);
+ }
- // Update the entity with modified values
- $entity = $dotEntity->all();
+ if ($fileEntity->getChecksum() === null || $fileEntity->getUpdated() > new DateTime('-5 minutes')) {
+ $fileEntity = $this->fetchFile(file: $fileEntity, propertyName: $propertyName, objectEntity: $objectEntity);
+ $fileEntity->setUpdated(new DateTime());
+ }
+ }
- return $this->extendEntity(entity: $entity, extend: $extend);
- }
+ $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();
+ }
+ }
- /**
- * 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
- * @return array The extended entity as an array
- * @throws Exception If property not found
- */
- public function extendEntity(array $entity, array $extend): array
- {
- // Convert entity to array if needed
- if (is_array($entity)) {
- $result = $entity;
- } else {
- $result = $entity->jsonSerialize();
- }
+ /**
+ * Retrieves 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 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
+ {
- // Process each property to extend
- foreach ($extend as $property) {
- $singularProperty = rtrim($property, 's');
-
- // Check if property exists
- if (array_key_exists(key: $property, array: $result) === true) {
- $value = $result[$property];
- if (empty($value)) {
- continue;
- }
- } elseif (array_key_exists(key: $singularProperty, array: $result)) {
- $value = $result[$singularProperty];
- } else {
- throw new Exception("Property '$property' or '$singularProperty' is not present in the entity.");
- }
-
- // Try to get mapper for property
- try {
- $mapper = $this->getMapper(objectType: $property);
- $propertyObject = $singularProperty;
-
- // Extend with related objects using specific mapper
- if (is_array($value) === true) {
- $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value);
- } else {
- $objectId = is_object(value: $value) ? $value->getId() : $value;
- $result[$property] = $mapper->find($objectId);
- }
- } catch (Exception $e) {
- // If no specific mapper found, try to look up values in default database
- try {
- if (is_array($value)) {
- // Handle array of values
- $extendedValues = [];
- foreach ($value as $val) {
- try {
- $found = $this->objectEntityMapper->find($val);
- if ($found) {
- $extendedValues[] = $found;
- }
- } catch (Exception $e) {
- continue;
- }
- }
- if (!empty($extendedValues)) {
- $result[$property] = $extendedValues;
- }
- } else {
- // Handle single value
- $found = $this->objectEntityMapper->find($value);
- if ($found) {
- $result[$property] = $found;
- }
- }
- } catch (Exception $e2) {
- // If lookup fails, keep original value
- continue;
- }
- }
- }
+ // Handle internal source
+ if ($register->getSource() === 'internal' || $register->getSource() === '') {
+ return $this->objectEntityMapper->findByUuid($register, $schema, $uuid);
+ }
- return $result;
- }
+ //@todo mongodb support
- /**
- * Get all registers extended with their schemas
- *
- * @return array The registers with schema data
- * @throws Exception If extension fails
- */
- public function getRegisters(): array
- {
- // Get all registers
- $registers = $this->registerMapper->findAll();
-
- // Convert to arrays
- $registers = array_map(function($object) {
- return $object->jsonSerialize();
- }, $registers);
-
- // Extend with schemas
- $extend = ['schemas'];
- if (empty($extend) === false) {
- $registers = array_map(function($object) use ($extend) {
- return $this->extendEntity(entity: $object, extend: $extend);
- }, $registers);
- }
+ throw new Exception('Unsupported source type');
+ }
- return $registers;
- }
+ /**
+ * 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, '.');
- /**
- * Get current register ID
- *
- * @return int The register ID
- */
- public function getRegister(): int
- {
- return $this->register;
- }
+ // Return the substring before the dot, or the original string if no dot is found
+ return $dotPosition !== false ? substr($input, 0, $dotPosition) : $input;
+ }
- /**
- * Set current register ID
- *
- * @param int $register The register ID to set
- */
- public function setRegister(int $register): void
- {
- $this->register = $register;
- }
+ /**
+ * 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, '/');
- /**
- * Get current schema ID
- *
- * @return int The schema ID
- */
- public function getSchema(): int
- {
- return $this->schema;
- }
+ // Return the substring after the last slash, or the original string if no slash is found
+ return $lastSlashPos !== false ? substr($input, $lastSlashPos + 1) : $input;
+ }
- /**
- * Set current schema ID
- *
- * @param int $schema The schema ID to set
- */
- public function setSchema(int $schema): void
- {
- $this->schema = $schema;
- }
+ /**
+ * 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;
+ }
+ }
- /**
- * Get the audit trail for a specific object
- *
- * @param string $id The object ID
- * @param int|null $register Optional register ID to override current register
- * @param int|null $schema Optional schema ID to override current schema
- * @return array The audit trail entries
- */
- public function getAuditTrail(string $id, ?int $register = null, ?int $schema = null): array
- {
- // Get the object to get its URI and UUID
- $object = $this->find($id);
+ 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 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
+ * @throws Exception If source type is unsupported
+ */
+ 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->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;
+ }
- // @todo this is not working, it fails to find the logs
+ //@todo mongodb support
+
+ throw new Exception('Unsupported source type');
+ }
+
+ /**
+ * 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);
+ $this->setRegister($register);
+ return $this;
+ }
+
+ // Return appropriate mapper based on object type
+ switch ($objectType) {
+ case 'register':
+ return $this->registerMapper;
+ case 'schema':
+ return $this->schemaMapper;
+ case 'objectEntity':
+ return $this->objectEntityMapper;
+ default:
+ throw new InvalidArgumentException("Unknown object type: $objectType");
+ }
+ }
+
+ /**
+ * Retrieves multiple objects of a specified type using their identifiers.
+ *
+ * Processes and cleans input IDs to ensure compatibility with the mapper.
+ *
+ * @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) {
+ if (is_object($id) && method_exists($id, 'getId')) {
+ return $id->getId();
+ } elseif (is_array($id) && isset($id['id'])) {
+ return $id['id'];
+ } else {
+ return $id;
+ }
+ }, $ids);
+
+ // Clean up URIs to get just the ID portion
+ $cleanedIds = array_map(function ($id) {
+ if (filter_var($id, FILTER_VALIDATE_URL)) {
+ $parts = explode('/', rtrim($id, '/'));
+ return end($parts);
+ }
+ return $id;
+ }, $processedIds);
+
+ // Get mapper and find objects
+ $mapper = $this->getMapper($objectType);
+ return $mapper->findMultiple($cleanedIds);
+ }
+
+ /**
+ * Renders an entity by replacing file and relation IDs with their respective objects.
+ *
+ * Expands files and relations within the entity based on the provided extend array.
+ *
+ * @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
+ {
+ // 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;
+ }
+
+ // Lets create a dot array of the entity
+ $dotEntity = new Dot($entity);
+
+ // loop through the files and replace the file ids with the file objects)
+ if (array_key_exists(key: 'files', array: $entity) === true && empty($entity['files']) === false) {
+ // Loop through the files array where key is dot notation path and value is file id
+ foreach ($entity['files'] as $path => $fileId) {
+ // Replace the value at the dot notation path with the file URL
+ // @todo: does not work
+// $dotEntity->set($path, $filesById[$fileId]->getUrl());
+ }
+ }
+
+ // Loop through the relations and replace the relation ids with the relation objects if extended
+ if (array_key_exists(key: 'relations', array: $entity) === true && empty($entity['relations']) === false) {
+ // loop through the relations and replace the relation ids with the relation objects
+ foreach ($entity['relations'] as $path => $relationId) {
+ // if the relation is not in the extend array, skip it
+ if (in_array(needle: $path, haystack: $extend) === false) {
+ continue;
+ }
+ // Replace the value at the dot notation path with the relation object
+ // @todo: does not work
+// $dotEntity->set($path, $this->getObject(register: $this->getRegister(), schema: $this->getSchema(), uuid: $relationId));
+ }
+ }
+
+ // Update the entity with modified values
+ $entity = $dotEntity->all();
+
+ 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
+ * @return array The extended entity as an array
+ * @throws Exception If property not found
+ */
+ public function extendEntity(array $entity, array $extend): array
+ {
+ // Convert entity to array if needed
+ if (is_array($entity)) {
+ $result = $entity;
+ } else {
+ $result = $entity->jsonSerialize();
+ }
+
+ // Process each property to extend
+ foreach ($extend as $property) {
+ $singularProperty = rtrim($property, 's');
+
+ // Check if property exists
+ if (array_key_exists(key: $property, array: $result) === true) {
+ $value = $result[$property];
+ if (empty($value)) {
+ continue;
+ }
+ } elseif (array_key_exists(key: $singularProperty, array: $result)) {
+ $value = $result[$singularProperty];
+ } else {
+ throw new Exception("Property '$property' or '$singularProperty' is not present in the entity.");
+ }
+
+ // Try to get mapper for property
+ try {
+ $mapper = $this->getMapper(objectType: $property);
+ $propertyObject = $singularProperty;
+
+ // Extend with related objects using specific mapper
+ if (is_array($value) === true) {
+ $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value);
+ } else {
+ $objectId = is_object(value: $value) ? $value->getId() : $value;
+ $result[$property] = $mapper->find($objectId);
+ }
+ } catch (Exception $e) {
+ // If no specific mapper found, try to look up values in default database
+ try {
+ if (is_array($value)) {
+ // Handle array of values
+ $extendedValues = [];
+ foreach ($value as $val) {
+ try {
+ $found = $this->objectEntityMapper->find($val);
+ if ($found) {
+ $extendedValues[] = $found;
+ }
+ } catch (Exception $e) {
+ continue;
+ }
+ }
+ if (!empty($extendedValues)) {
+ $result[$property] = $extendedValues;
+ }
+ } else {
+ // Handle single value
+ $found = $this->objectEntityMapper->find($value);
+ if ($found) {
+ $result[$property] = $found;
+ }
+ }
+ } catch (Exception $e2) {
+ // If lookup fails, keep original value
+ continue;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 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();
+
+ // Convert to arrays
+ $registers = array_map(function ($object) {
+ return $object->jsonSerialize();
+ }, $registers);
+
+ // Extend with schemas
+ $extend = ['schemas'];
+ if (empty($extend) === false) {
+ $registers = array_map(function ($object) use ($extend) {
+ return $this->extendEntity(entity: $object, extend: $extend);
+ }, $registers);
+ }
+
+ return $registers;
+ }
+
+ /**
+ * Retrieves the current register ID.
+ *
+ * @return int The current register ID.
+ */
+ public function getRegister(): int
+ {
+ return $this->register;
+ }
+
+ /**
+ * Sets the current register ID.
+ *
+ * @param int $register The register ID to set.
+ */
+ public function setRegister(int $register): void
+ {
+ $this->register = $register;
+ }
+
+ /**
+ * Retrieves the current schema ID.
+ *
+ * @return int The current schema ID.
+ */
+ public function getSchema(): int
+ {
+ return $this->schema;
+ }
+
+ /**
+ * 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
+ *
+ * @param string $id The object ID
+ * @param int|null $register Optional register ID to override current register
+ * @param int|null $schema Optional schema ID to override current schema
+ * @return array The audit trail entries
+ */
+ public function getAuditTrail(string $id, ?int $register = null, ?int $schema = null): array
+ {
+ // Get the object to get its URI and UUID
+ $object = $this->find($id);
+
+ // @todo this is not working, it fails to find the logs
$auditTrails = $this->auditTrailMapper->findAll(filters: ['object' => $object->getId()]);
-
- return $auditTrails;
- }
- /**
- * Get all relations for a specific object
- * Returns objects that link to this object (incoming references)
- *
- * @param string $id The object ID
- * @param int|null $register Optional register ID to override current register
- * @param int|null $schema Optional schema ID to override current schema
- * @return array The objects that reference this object
- */
- public function getRelations(string $id, ?int $register = null, ?int $schema = null): array
- {
- $register = $register ?? $this->getRegister();
- $schema = $schema ?? $this->getSchema();
-
- // Get the object to get its URI and UUID
- $object = $this->find($id);
-
- // Find objects that reference this object's URI or UUID
- $referencingObjects = $this->objectEntityMapper->findByRelationUri(
- search: $object->getUuid(),
- partialMatch: true
- );
+ return $auditTrails;
+ }
- // Filter out self-references if any
- return array_filter($referencingObjects, function($referencingObject) use ($id) {
- return $referencingObject->getUuid() !== $id;
- });
- }
+ /**
+ * Get all relations for a specific object
+ * Returns objects that link to this object (incoming references)
+ *
+ * @param string $id The object ID
+ * @param int|null $register Optional register ID to override current register
+ * @param int|null $schema Optional schema ID to override current schema
+ * @return array The objects that reference this object
+ */
+ public function getRelations(string $id, ?int $register = null, ?int $schema = null): array
+ {
+ $register = $register ?? $this->getRegister();
+ $schema = $schema ?? $this->getSchema();
- /**
- * Get all uses of a specific object
- * Returns objects that this object links to (outgoing references)
- *
- * @param string $id The object ID
- * @param int|null $register Optional register ID to override current register
- * @param int|null $schema Optional schema ID to override current schema
- * @return array The objects this object references
- */
- public function getUses(string $id, ?int $register = null, ?int $schema = null): array
- {
- // First get the object to access its relations
- $object = $this->find($id);
- $relations = $object->getRelations() ?? [];
-
- // Get all referenced objects
- $referencedObjects = [];
- foreach ($relations as $path => $relationId) {
- $referencedObjects[$path] = $this->objectEntityMapper->find($relationId);
-
- if($referencedObjects[$path] === null){
- $referencedObjects[$path] = $relationId;
- }
- }
+ // Get the object to get its URI and UUID
+ $object = $this->find($id);
- return $referencedObjects;
- }
+ // Find objects that reference this object's URI or UUID
+ $referencingObjects = $this->objectEntityMapper->findByRelationUri(
+ search: $object->getUuid(),
+ partialMatch: true
+ );
+
+ // Filter out self-references if any
+ return array_filter($referencingObjects, function($referencingObject) use ($id) {
+ return $referencingObject->getUuid() !== $id;
+ });
+ }
+
+ /**
+ * Get all uses of a specific object
+ * Returns objects that this object links to (outgoing references)
+ *
+ * @param string $id The object ID
+ * @param int|null $register Optional register ID to override current register
+ * @param int|null $schema Optional schema ID to override current schema
+ * @return array The objects this object references
+ */
+ public function getUses(string $id, ?int $register = null, ?int $schema = null): array
+ {
+ // First get the object to access its relations
+ $object = $this->find($id);
+ $relations = $object->getRelations() ?? [];
+
+ // Get all referenced objects
+ $referencedObjects = [];
+ foreach ($relations as $path => $relationId) {
+ $referencedObjects[$path] = $this->objectEntityMapper->find($relationId);
+
+ if($referencedObjects[$path] === null){
+ $referencedObjects[$path] = $relationId;
+ }
+ }
+
+ return $referencedObjects;
+ }
}
diff --git a/src/modals/schema/EditSchemaProperty.vue b/src/modals/schema/EditSchemaProperty.vue
index b46abb518d..eb59593595 100644
--- a/src/modals/schema/EditSchemaProperty.vue
+++ b/src/modals/schema/EditSchemaProperty.vue
@@ -34,7 +34,8 @@ import { navigationStore, schemaStore } from '../../store/store.js'