From 36e25f967be147731e73d08dda5cd446b61c08c3 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Tue, 3 Mar 2026 22:10:46 -0500 Subject: [PATCH] refactor: standardize design Signed-off-by: Sebastian Krupinski --- composer.json | 6 +- lib/Module.php | 44 +- .../Personal/CollectionProperties.php | 40 + lib/Providers/Personal/CollectionResource.php | 63 ++ lib/Providers/Personal/EntityProperties.php | 59 ++ lib/Providers/Personal/EntityResource.php | 63 ++ lib/Providers/Personal/NodeCollection.php | 176 ----- lib/Providers/Personal/NodeEntity.php | 226 ------ lib/Providers/Personal/PersonalService.php | 696 ++++++++---------- lib/Providers/Provider.php | 233 ++---- lib/Store/BlobStore.php | 2 +- lib/Store/MetaStore.php | 218 +++--- 12 files changed, 750 insertions(+), 1076 deletions(-) create mode 100644 lib/Providers/Personal/CollectionProperties.php create mode 100644 lib/Providers/Personal/CollectionResource.php create mode 100644 lib/Providers/Personal/EntityProperties.php create mode 100644 lib/Providers/Personal/EntityResource.php delete mode 100644 lib/Providers/Personal/NodeCollection.php delete mode 100644 lib/Providers/Personal/NodeEntity.php diff --git a/composer.json b/composer.json index 9cb13f8..2a4f01a 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "ktrix/file-provider-local", + "name": "ktrix/provider-local-documents", "type": "project", "authors": [ { @@ -12,7 +12,7 @@ "platform": { "php": "8.2" }, - "autoloader-suffix": "FileProviderLocal", + "autoloader-suffix": "ProviderLocalDocuments", "vendor-dir": "lib/vendor" }, "require": { @@ -20,7 +20,7 @@ }, "autoload": { "psr-4": { - "KTXM\\FileProviderLocal\\": "lib/" + "KTXM\\ProviderLocalDocuments\\": "lib/" } } } diff --git a/lib/Module.php b/lib/Module.php index e3df8c1..62bdccf 100644 --- a/lib/Module.php +++ b/lib/Module.php @@ -1,53 +1,57 @@ [ - 'label' => 'Access File Provider Local', - 'description' => 'View and access the local file provider module', + 'provider_local_documents' => [ + 'label' => 'Access Local Documents Storage Provider', + 'description' => 'View and access the local documents storage provider module', 'group' => 'File Providers' ], ]; @@ -55,17 +59,17 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface public function boot(): void { - $this->providerManager->register(ProviderInterface::TYPE_FILES, 'default', Provider::class); + $this->providerManager->register(ProviderInterface::TYPE_DOCUMENT, 'default', Provider::class); } public function registerBI(): array { return [ - 'handle' => $this->handle(), - 'namespace' => 'FileProviderLocal', - 'version' => $this->version(), - 'label' => $this->label(), - 'author' => $this->author(), - 'description' => $this->description() + 'handle' => self::MODULE_HANDLE, + 'namespace' => self::MODULE_NAMESPACE, + 'version' => self::MODULE_VERSION, + 'label' => self::MODULE_LABEL, + 'author' => self::MODULE_AUTHOR, + 'description' => self::MODULE_DESCRIPTION ]; } } diff --git a/lib/Providers/Personal/CollectionProperties.php b/lib/Providers/Personal/CollectionProperties.php new file mode 100644 index 0000000..7620fce --- /dev/null +++ b/lib/Providers/Personal/CollectionProperties.php @@ -0,0 +1,40 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalDocuments\Providers\Personal; + +use KTXF\Resource\Documents\Collection\CollectionPropertiesMutableAbstract; + +class CollectionProperties extends CollectionPropertiesMutableAbstract { + + /** + * Converts store document values into collection properties. + */ + public function fromStore(array|object $data): static { + + if (is_object($data)) { + $data = (array) $data; + } + + if (isset($data['label'])) { + $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['label']; + } + + return $this; + } + + /** + * Converts collection properties into store document values. + */ + public function toStore(): array { + return array_filter([ + 'label' => $this->getLabel(), + ], static fn($value) => $value !== null); + } +} diff --git a/lib/Providers/Personal/CollectionResource.php b/lib/Providers/Personal/CollectionResource.php new file mode 100644 index 0000000..a9fc41b --- /dev/null +++ b/lib/Providers/Personal/CollectionResource.php @@ -0,0 +1,63 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalDocuments\Providers\Personal; + +use KTXF\Resource\Documents\Collection\CollectionMutableAbstract; + +class CollectionResource extends CollectionMutableAbstract { + + public function __construct( + string $provider = 'default', + string|int $service = 'personal', + ) { + parent::__construct($provider, $service); + } + + public function fromStore(array|object $data): self + { + if (is_object($data)) { + $data = (array) $data; + } + + $this->data[static::JSON_PROPERTY_COLLECTION] = $data['cid'] ?? null; + $this->data[static::JSON_PROPERTY_IDENTIFIER] = $data['nid']; + $this->data[static::JSON_PROPERTY_CREATED] = $data['created'] ?? null; + $this->data[static::JSON_PROPERTY_MODIFIED] = $data['modified'] ?? null; + $this->data[static::JSON_PROPERTY_SIGNATURE] = $data['signature'] ?? null; + $this->getProperties()->fromStore($data['properties'] ?? []); + + return $this; + } + + public function toStore(): array + { + $properties = $this->getProperties(); + + $data = array_filter([ + 'cid' => $this->data[static::JSON_PROPERTY_COLLECTION] ?? null, + 'nid' => $this->data[static::JSON_PROPERTY_IDENTIFIER] ?? null, + 'created' => $this->data[static::JSON_PROPERTY_CREATED] ?? null, + 'modified' => $this->data[static::JSON_PROPERTY_MODIFIED] ?? null, + 'signature' => $this->data[static::JSON_PROPERTY_SIGNATURE] ?? null, + 'properties' => $properties ? $properties->toStore() : null, + ], static fn($value) => $value !== null); + + return $data; + } + + public function getProperties(): CollectionProperties { + if (!isset($this->properties)) { + $this->properties = new CollectionProperties([]); + } + + return $this->properties; + } + +} diff --git a/lib/Providers/Personal/EntityProperties.php b/lib/Providers/Personal/EntityProperties.php new file mode 100644 index 0000000..4dd731a --- /dev/null +++ b/lib/Providers/Personal/EntityProperties.php @@ -0,0 +1,59 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalDocuments\Providers\Personal; + +use KTXF\Resource\Documents\Entity\EntityPropertiesMutableAbstract; + +class EntityProperties extends EntityPropertiesMutableAbstract { + + /** + * Converts store document values into entity properties. + */ + public function fromStore(array|object $data): static { + + if (is_object($data)) { + $data = (array) $data; + } + + if (isset($data['size'])) { + $this->data[self::JSON_PROPERTY_SIZE] = (int) $data['size']; + } + + if (isset($data['label'])) { + $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['label']; + } + + if (isset($data['mime'])) { + $this->data[self::JSON_PROPERTY_MIME] = (string) $data['mime']; + } + + if (isset($data['format'])) { + $this->data[self::JSON_PROPERTY_FORMAT] = (string) $data['format']; + } + + if (isset($data['encoding'])) { + $this->data[self::JSON_PROPERTY_ENCODING] = (string) $data['encoding']; + } + + return $this; + } + + /** + * Converts entity properties into store document values. + */ + public function toStore(): array { + return array_filter([ + 'label' => $this->data[self::JSON_PROPERTY_LABEL], + 'mime' => $this->data[self::JSON_PROPERTY_MIME] ?? 'application/octet-stream', + 'format' => $this->data[self::JSON_PROPERTY_FORMAT] ?? null, + 'encoding' => $this->data[self::JSON_PROPERTY_ENCODING] ?? null, + ], static fn($value) => $value !== null); + } +} diff --git a/lib/Providers/Personal/EntityResource.php b/lib/Providers/Personal/EntityResource.php new file mode 100644 index 0000000..0bb66dc --- /dev/null +++ b/lib/Providers/Personal/EntityResource.php @@ -0,0 +1,63 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalDocuments\Providers\Personal; + +use KTXF\Resource\Documents\Entity\EntityMutableAbstract; + +class EntityResource extends EntityMutableAbstract { + + public function __construct( + string $provider = 'default', + string|int $service = 'personal', + ) { + parent::__construct($provider, $service); + } + + public function fromStore(array|object $data): self + { + if (is_object($data)) { + $data = (array) $data; + } + + $this->data[static::JSON_PROPERTY_COLLECTION] = $data['cid'] ?? null; + $this->data[static::JSON_PROPERTY_IDENTIFIER] = $data['nid']; + $this->data[static::JSON_PROPERTY_CREATED] = $data['created'] ?? null; + $this->data[static::JSON_PROPERTY_MODIFIED] = $data['modified'] ?? null; + $this->data[static::JSON_PROPERTY_SIGNATURE] = $data['signature'] ?? null; + $this->getProperties()->fromStore($data['properties'] ?? []); + + return $this; + } + + public function toStore(): array + { + $properties = $this->getProperties(); + + $data = array_filter([ + 'cid' => $this->data[static::JSON_PROPERTY_COLLECTION] ?? null, + 'nid' => $this->data[static::JSON_PROPERTY_IDENTIFIER] ?? null, + 'created' => $this->data[static::JSON_PROPERTY_CREATED] ?? null, + 'modified' => $this->data[static::JSON_PROPERTY_MODIFIED] ?? null, + 'signature' => $this->data[static::JSON_PROPERTY_SIGNATURE] ?? null, + 'properties' => $properties ? $properties->toStore() : null, + ], static fn($value) => $value !== null); + + return $data; + } + + public function getProperties(): EntityProperties { + if (!isset($this->properties)) { + $this->properties = new EntityProperties([]); + } + + return $this->properties; + } + +} diff --git a/lib/Providers/Personal/NodeCollection.php b/lib/Providers/Personal/NodeCollection.php deleted file mode 100644 index 96194ca..0000000 --- a/lib/Providers/Personal/NodeCollection.php +++ /dev/null @@ -1,176 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\FileProviderLocal\Providers\Personal; - -use DateTimeImmutable; -use KTXF\Files\Node\INodeCollectionBase; -use KTXF\Files\Node\INodeCollectionMutable; -use KTXF\Files\Node\NodeType; - -/** - * NodeCollection implementation - represents a folder/collection in the file system - */ -class NodeCollection implements INodeCollectionBase, INodeCollectionMutable { - - // node system properties - private string|null $tenantId = null; - private string|null $userId = null; - private string|int|null $nodeIn = null; - private string|int|null $nodeId = null; - private string|null $nodeCreatedBy = null; - private DateTimeImmutable|null $nodeCreatedOn = null; - private string|null $nodeModifiedBy = null; - private DateTimeImmutable|null $nodeModifiedOn = null; - private string|null $nodeOwner = null; - private string|null $nodeSignature = null; - // node specific properties - private string|null $nodeLabel = null; - - public function jsonSerialize(): mixed { - return [ - // node meta properties - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_IN => $this->nodeIn, - self::JSON_PROPERTY_ID => $this->nodeId, - self::JSON_PROPERTY_CREATED_BY => $this->nodeCreatedBy, - self::JSON_PROPERTY_CREATED_ON => $this->nodeCreatedOn?->format('c'), - self::JSON_PROPERTY_MODIFIED_BY => $this->nodeModifiedBy, - self::JSON_PROPERTY_MODIFIED_ON => $this->nodeModifiedOn?->format('c'), - self::JSON_PROPERTY_OWNER => $this->nodeOwner, - self::JSON_PROPERTY_SIGNATURE => $this->nodeSignature, - // node specific properties - self::JSON_PROPERTY_LABEL => $this->nodeLabel, - ]; - } - - public function jsonDeserialize(array|string $data): static { - if (is_string($data)) { - $data = json_decode($data, true); - } - // node meta properties - $this->nodeIn = $data[self::JSON_PROPERTY_IN] ?? null; - $this->nodeId = $data[self::JSON_PROPERTY_ID] ?? null; - $this->nodeCreatedBy = $data[self::JSON_PROPERTY_CREATED_BY] ?? null; - $this->nodeCreatedOn = isset($data[self::JSON_PROPERTY_CREATED_ON]) - ? new DateTimeImmutable($data[self::JSON_PROPERTY_CREATED_ON]) - : null; - $this->nodeModifiedBy = $data[self::JSON_PROPERTY_MODIFIED_BY] ?? null; - $this->nodeModifiedOn = isset($data[self::JSON_PROPERTY_MODIFIED_ON]) - ? new DateTimeImmutable($data[self::JSON_PROPERTY_MODIFIED_ON]) - : null; - $this->nodeOwner = $data[self::JSON_PROPERTY_OWNER] ?? null; - $this->nodeSignature = $data[self::JSON_PROPERTY_SIGNATURE] ?? null; - // node specific properties - $this->nodeLabel = $data[self::JSON_PROPERTY_LABEL] ?? null; - - return $this; - } - - public function fromStore(array|object $document): self { - if (is_object($document)) { - $document = (array) $document; - } - // node system properties - $this->tenantId = $document['tid'] ?? null; - $this->userId = $document['uid'] ?? null; - $this->nodeId = $document['nid'] ?? null; - $this->nodeIn = $document['pid'] ?? null; - $this->nodeCreatedBy = $document['createdBy'] ?? null; - $this->nodeCreatedOn = isset($document['createdOn']) - ? new DateTimeImmutable($document['createdOn']) - : null; - $this->nodeModifiedBy = $document['modifiedBy'] ?? null; - $this->nodeModifiedOn = isset($document['modifiedOn']) - ? new DateTimeImmutable($document['modifiedOn']) - : null; - $this->nodeOwner = $document['owner'] ?? null; - $this->nodeSignature = $document['signature'] ?? md5(json_encode([ - $this->nodeId, NodeType::Collection->value, $document['modifiedOn'] ?? '' - ])); - // node specific properties - $this->nodeLabel = $document['label'] ?? null; - - return $this; - } - - public function toStore(): array { - $now = date('c'); - return [ - // node system properties - 'tid' => $this->tenantId, - 'uid' => $this->userId, - 'nid' => $this->nodeId, - 'pid' => $this->nodeIn, - 'type' => NodeType::Collection->value, - 'createdBy' => $this->nodeCreatedBy, - 'createdOn' => $this->nodeCreatedOn?->format('c') ?? $now, - 'modifiedBy' => $this->nodeModifiedBy, - 'modifiedOn' => $this->nodeModifiedOn?->format('c') ?? $now, - 'owner' => $this->nodeOwner, - 'signature' => $this->nodeSignature, - // node specific properties - 'label' => $this->nodeLabel, - ]; - } - - // Immutable properties - - public function in(): string|int|null { - return $this->nodeIn; - } - - public function id(): string|int { - return $this->nodeId ?? ''; - } - - public function type(): NodeType { - return NodeType::Collection; - } - - public function createdBy(): string|null { - return $this->nodeCreatedBy; - } - - public function createdOn(): DateTimeImmutable|null { - return $this->nodeCreatedOn; - } - - public function modifiedBy(): string|null { - return $this->nodeModifiedBy; - } - - public function modifiedOn(): DateTimeImmutable|null { - return $this->nodeModifiedOn; - } - - public function signature(): string|null { - return $this->nodeSignature; - } - - public function isCollection(): bool { - return true; - } - - public function isEntity(): bool { - return false; - } - - // Mutable properties - - public function getLabel(): string|null { - return $this->nodeLabel; - } - - public function setLabel(string $value): static { - $this->nodeLabel = $value; - return $this; - } - -} diff --git a/lib/Providers/Personal/NodeEntity.php b/lib/Providers/Personal/NodeEntity.php deleted file mode 100644 index 894077b..0000000 --- a/lib/Providers/Personal/NodeEntity.php +++ /dev/null @@ -1,226 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\FileProviderLocal\Providers\Personal; - -use DateTimeImmutable; -use KTXF\Files\Node\INodeEntityBase; -use KTXF\Files\Node\INodeEntityMutable; -use KTXF\Files\Node\NodeType; - -/** - * NodeEntity implementation - represents a file/entity in the file system - */ -class NodeEntity implements INodeEntityBase, INodeEntityMutable { - - // node system properties - private string|null $tenantId = null; - private string|null $userId = null; - private string|int|null $nodeIn = null; - private string|int|null $nodeId = null; - private string|null $nodeCreatedBy = null; - private DateTimeImmutable|null $nodeCreatedOn = null; - private string|null $nodeModifiedBy = null; - private DateTimeImmutable|null $nodeModifiedOn = null; - private string|null $nodeOwner = null; - private string|null $nodeSignature = null; - // node specific properties - private string|null $nodeLabel = null; - private string|null $nodeMime = null; - private string|null $nodeFormat = null; - private string|null $nodeEncoding = null; - private int $nodeSize = 0; - - public function jsonSerialize(): mixed { - return [ - // node meta properties - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_IN => $this->nodeIn, - self::JSON_PROPERTY_ID => $this->nodeId, - self::JSON_PROPERTY_CREATED_BY => $this->nodeCreatedBy, - self::JSON_PROPERTY_CREATED_ON => $this->nodeCreatedOn?->format('c'), - self::JSON_PROPERTY_MODIFIED_BY => $this->nodeModifiedBy, - self::JSON_PROPERTY_MODIFIED_ON => $this->nodeModifiedOn?->format('c'), - self::JSON_PROPERTY_OWNER => $this->nodeOwner, - self::JSON_PROPERTY_SIGNATURE => $this->nodeSignature, - self::JSON_PROPERTY_SIZE => $this->nodeSize, - // node specific properties - self::JSON_PROPERTY_LABEL => $this->nodeLabel, - self::JSON_PROPERTY_MIME => $this->nodeMime, - self::JSON_PROPERTY_FORMAT => $this->nodeFormat, - self::JSON_PROPERTY_ENCODING => $this->nodeEncoding, - ]; - } - - public function jsonDeserialize(array|string $data): static { - if (is_string($data)) { - $data = json_decode($data, true); - } - // node meta properties - $this->nodeIn = $data[self::JSON_PROPERTY_IN] ?? null; - $this->nodeId = $data[self::JSON_PROPERTY_ID] ?? null; - $this->nodeCreatedBy = $data[self::JSON_PROPERTY_CREATED_BY] ?? null; - $this->nodeCreatedOn = isset($data[self::JSON_PROPERTY_CREATED_ON]) - ? new DateTimeImmutable($data[self::JSON_PROPERTY_CREATED_ON]) - : null; - $this->nodeModifiedBy = $data[self::JSON_PROPERTY_MODIFIED_BY] ?? null; - $this->nodeModifiedOn = isset($data[self::JSON_PROPERTY_MODIFIED_ON]) - ? new DateTimeImmutable($data[self::JSON_PROPERTY_MODIFIED_ON]) - : null; - $this->nodeOwner = $data[self::JSON_PROPERTY_OWNER] ?? null; - $this->nodeSignature = $data[self::JSON_PROPERTY_SIGNATURE] ?? null; - $this->nodeSize = $data[self::JSON_PROPERTY_SIZE] ?? 0; - // node specific properties - $this->nodeLabel = $data[self::JSON_PROPERTY_LABEL] ?? null; - $this->nodeMime = $data[self::JSON_PROPERTY_MIME] ?? null; - $this->nodeFormat = $data[self::JSON_PROPERTY_FORMAT] ?? null; - $this->nodeEncoding = $data[self::JSON_PROPERTY_ENCODING] ?? null; - - return $this; - } - - public function fromStore(array|object $document): self { - if (is_object($document)) { - $document = (array) $document; - } - // node system properties - $this->tenantId = $document['tid'] ?? null; - $this->userId = $document['uid'] ?? null; - $this->nodeId = $document['nid'] ?? null; - $this->nodeIn = $document['pid'] ?? null; - $this->nodeCreatedBy = $document['createdBy'] ?? null; - $this->nodeCreatedOn = isset($document['createdOn']) - ? new DateTimeImmutable($document['createdOn']) - : null; - $this->nodeModifiedBy = $document['modifiedBy'] ?? null; - $this->nodeModifiedOn = isset($document['modifiedOn']) - ? new DateTimeImmutable($document['modifiedOn']) - : null; - $this->nodeOwner = $document['owner'] ?? null; - $this->nodeSignature = $document['signature'] ?? md5(json_encode([ - $this->nodeId, NodeType::Entity->value, $document['modifiedOn'] ?? '' - ])); - $this->nodeSize = $document['size'] ?? 0; - // node specific properties - $this->nodeLabel = $document['label'] ?? null; - $this->nodeMime = $document['mime'] ?? null; - $this->nodeFormat = $document['format'] ?? null; - $this->nodeEncoding = $document['encoding'] ?? null; - - return $this; - } - - public function toStore(): array { - $now = date('c'); - return [ - // node system properties - 'tid' => $this->tenantId, - 'uid' => $this->userId, - 'nid' => $this->nodeId, - 'pid' => $this->nodeIn, - 'type' => NodeType::Entity->value, - 'createdBy' => $this->nodeCreatedBy, - 'createdOn' => $this->nodeCreatedOn?->format('c') ?? $now, - 'modifiedBy' => $this->nodeModifiedBy, - 'modifiedOn' => $this->nodeModifiedOn?->format('c') ?? $now, - 'owner' => $this->nodeOwner, - 'signature' => $this->nodeSignature, - 'size' => $this->nodeSize, - // node specific properties - 'label' => $this->nodeLabel, - 'mime' => $this->nodeMime, - 'format' => $this->nodeFormat, - 'encoding' => $this->nodeEncoding, - ]; - } - - // Immutable properties - - public function in(): string|int|null { - return $this->nodeIn; - } - - public function id(): string|int { - return $this->nodeId ?? ''; - } - - public function type(): NodeType { - return NodeType::Entity; - } - - public function createdBy(): string|null { - return $this->nodeCreatedBy; - } - - public function createdOn(): DateTimeImmutable|null { - return $this->nodeCreatedOn; - } - - public function modifiedBy(): string|null { - return $this->nodeModifiedBy; - } - - public function modifiedOn(): DateTimeImmutable|null { - return $this->nodeModifiedOn; - } - - public function signature(): string|null { - return $this->nodeSignature; - } - - public function size(): int { - return $this->nodeSize; - } - - public function isCollection(): bool { - return false; - } - - public function isEntity(): bool { - return true; - } - - // Mutable properties - - public function getLabel(): string|null { - return $this->nodeLabel; - } - - public function setLabel(string $value): static { - $this->nodeLabel = $value; - return $this; - } - - public function getMime(): string|null { - return $this->nodeMime; - } - - public function setMime(string $value): static { - $this->nodeMime = $value; - return $this; - } - - public function getFormat(): string|null { - return $this->nodeFormat; - } - - public function setFormat(string $value): static { - $this->nodeFormat = $value; - return $this; - } - - public function getEncoding(): string|null { - return $this->nodeEncoding; - } - - public function setEncoding(string $value): static { - $this->nodeEncoding = $value; - return $this; - } -} diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index 796b2db..6336477 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -7,17 +7,8 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\FileProviderLocal\Providers\Personal; +namespace KTXM\ProviderLocalDocuments\Providers\Personal; -use KTXF\Files\Node\INodeBase; -use KTXF\Files\Node\INodeCollectionBase; -use KTXF\Files\Node\INodeCollectionMutable; -use KTXF\Files\Node\INodeEntityBase; -use KTXF\Files\Node\INodeEntityMutable; -use KTXF\Files\Node\NodeType; -use KTXF\Files\Service\IServiceBase; -use KTXF\Files\Service\IServiceCollectionMutable; -use KTXF\Files\Service\IServiceEntityMutable; use KTXF\Resource\Exceptions\InvalidParameterException; use KTXF\Resource\Filter\Filter; use KTXF\Resource\Filter\IFilter; @@ -26,18 +17,26 @@ use KTXF\Resource\Range\RangeTally; use KTXF\Resource\Range\RangeType; use KTXF\Resource\Sort\ISort; use KTXF\Resource\Sort\Sort; -use KTXM\FileProviderLocal\Store\BlobStore; -use KTXM\FileProviderLocal\Store\MetaStore; +use KTXM\ProviderLocalDocuments\Store\BlobStore; +use KTXM\ProviderLocalDocuments\Store\MetaStore; use KTXF\Blob\Signature; +use KTXF\Files\Node\NodeType; +use KTXF\Resource\Delta\Delta; +use KTXF\Resource\Documents\Collection\CollectionMutableInterface; +use KTXF\Resource\Documents\Entity\EntityMutableInterface; +use KTXF\Resource\Documents\Service\ServiceBaseInterface; +use KTXF\Resource\Documents\Service\ServiceCollectionMutableInterface; +use KTXF\Resource\Documents\Service\ServiceEntityMutableInterface; +use KTXM\ProviderLocalDocuments\Providers\Personal\CollectionResource; -class PersonalService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable { +class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { public const ROOT_ID = '00000000-0000-0000-0000-000000000000'; - public const ROOT_LABEL = 'Personal Files'; + public const ROOT_LABEL = 'Personal Local Documents'; - protected const SERVICE_ID = 'personal'; - protected const SERVICE_LABEL = 'Personal Files Service'; - protected const SERVICE_PROVIDER = 'default'; + protected const PROVIDER_IDENTIFIER = 'default'; + protected const SERVICE_IDENTIFIER = 'personal'; + protected const SERVICE_LABEL = 'Personal Documents Storage'; protected array $serviceCollectionCache = []; protected array $serviceEntityCache = []; @@ -46,73 +45,45 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi protected ?bool $serviceEnabled = true; protected ?string $serviceRoot = null; - protected array $serviceAbilities = [ - // Collection capabilities + private array $serviceAbilities = [ self::CAPABILITY_COLLECTION_LIST => true, self::CAPABILITY_COLLECTION_LIST_FILTER => [ - 'id' => 'a:10:64:192', - 'label' => 's:100:256:771', + self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:100:256:256', ], self::CAPABILITY_COLLECTION_LIST_SORT => [ - 'label', - 'created', - 'modified' + self::CAPABILITY_COLLECTION_SORT_LABEL, ], self::CAPABILITY_COLLECTION_EXTANT => true, self::CAPABILITY_COLLECTION_FETCH => true, - self::CAPABILITY_COLLECTION_CREATE => true, - self::CAPABILITY_COLLECTION_MODIFY => true, - self::CAPABILITY_COLLECTION_DESTROY => true, - self::CAPABILITY_COLLECTION_COPY => true, - self::CAPABILITY_COLLECTION_MOVE => true, - // Entity capabilities + self::CAPABILITY_COLLECTION_CREATE => true, + self::CAPABILITY_COLLECTION_UPDATE => true, + self::CAPABILITY_COLLECTION_DELETE => true, self::CAPABILITY_ENTITY_LIST => true, self::CAPABILITY_ENTITY_LIST_FILTER => [ - 'id' => 'a:10:64:192', - 'label' => 's:100:256:771', - 'mimeType' => 's:100:256:771', + self::CAPABILITY_ENTITY_FILTER_ALL => 's:200:256:256', + self::CAPABILITY_ENTITY_FILTER_ID => 's:100:256:256', + self::CAPABILITY_ENTITY_FILTER_URID => 's:100:256:256', + self::CAPABILITY_ENTITY_FILTER_LABEL => 's:100:256:256', ], self::CAPABILITY_ENTITY_LIST_SORT => [ - 'label', - 'size', - 'created', - 'modified' + self::CAPABILITY_ENTITY_SORT_ID, + self::CAPABILITY_ENTITY_SORT_LABEL, ], self::CAPABILITY_ENTITY_LIST_RANGE => [ - 'tally' => ['absolute', 'relative'] + self::CAPABILITY_ENTITY_RANGE_TALLY => [ + self::CAPABILITY_ENTITY_RANGE_TALLY_ABSOLUTE, + self::CAPABILITY_ENTITY_RANGE_TALLY_RELATIVE + ], + self::CAPABILITY_ENTITY_RANGE_DATE => true, ], self::CAPABILITY_ENTITY_DELTA => true, self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, - self::CAPABILITY_ENTITY_READ => true, - self::CAPABILITY_ENTITY_READ_STREAM => true, - self::CAPABILITY_ENTITY_READ_CHUNK => true, self::CAPABILITY_ENTITY_CREATE => true, - self::CAPABILITY_ENTITY_MODIFY => true, - self::CAPABILITY_ENTITY_DESTROY => true, - self::CAPABILITY_ENTITY_COPY => true, - self::CAPABILITY_ENTITY_MOVE => true, + self::CAPABILITY_ENTITY_UPDATE => true, + self::CAPABILITY_ENTITY_DELETE => true, + self::CAPABILITY_ENTITY_READ => true, self::CAPABILITY_ENTITY_WRITE => true, - self::CAPABILITY_ENTITY_WRITE_STREAM => true, - self::CAPABILITY_ENTITY_WRITE_CHUNK => true, - // Node capabilities (recursive/unified) - self::CAPABILITY_NODE_LIST => true, - self::CAPABILITY_NODE_LIST_FILTER => [ - 'id' => 'a:10:64:192', - 'label' => 's:100:256:771', - 'nodeType' => 's:10:20:192', - 'mimeType' => 's:100:256:771', - ], - self::CAPABILITY_NODE_LIST_SORT => [ - 'label', - 'nodeType', - 'size', - 'modified' - ], - self::CAPABILITY_NODE_LIST_RANGE => [ - 'tally' => ['absolute', 'relative'] - ], - self::CAPABILITY_NODE_DELTA => true, ]; public function __construct( @@ -120,18 +91,7 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi private readonly BlobStore $blobStore, ) {} - public function jsonSerialize(): mixed { - return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_PROVIDER => self::SERVICE_PROVIDER, - self::JSON_PROPERTY_ID => self::SERVICE_ID, - self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL, - self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, - self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, - ]; - } - - public function init(string $tenantId, string $userId, string $root): self { + public function initialize(string $tenantId, string $userId, string $root): self { $this->serviceTenantId = $tenantId; $this->serviceUserId = $userId; $this->serviceRoot = $root; @@ -140,21 +100,34 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi $this->blobStore->configureRoot($root); $this->serviceCollectionCache = []; - $root = new NodeCollection(); + $root = new CollectionResource(); $root->fromStore([ 'tid' => $tenantId, 'uid' => $userId, - 'nid' => self::ROOT_ID, - 'pid' => null, - 'createdBy' => $userId, - 'createdOn' => (new \DateTimeImmutable())->format('c'), - 'owner' => $userId, - 'label' => self::ROOT_LABEL, + 'cid' => self::ROOT_ID, + 'nid' => null, + 'properties' => [ + 'label' => self::ROOT_LABEL, + ], ]); $this->serviceCollectionCache[self::ROOT_ID] = $root; return $this; } + + public function jsonSerialize(): array { + return array_filter([ + self::JSON_PROPERTY_TYPE => self::JSON_TYPE, + self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, + self::JSON_PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER, + self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL, + self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, + self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, + self::JSON_PROPERTY_LOCATION => null, + self::JSON_PROPERTY_IDENTITY => null, + self::JSON_PROPERTY_AUXILIARY => [], + ], fn($v) => $v !== null); + } public function capable(string $value): bool { if (isset($this->serviceAbilities[$value])) { @@ -167,12 +140,12 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $this->serviceAbilities; } - public function in(): string { - return self::SERVICE_PROVIDER; - } + public function provider(): string { + return self::PROVIDER_IDENTIFIER; + } - public function id(): string { - return self::SERVICE_ID; + public function identifier(): string { + return self::SERVICE_IDENTIFIER; } public function getLabel(): string { @@ -183,11 +156,25 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return (bool)$this->serviceEnabled; } - // ==================== Collection Methods (IServiceBase) ==================== + public function setEnabled(bool $enabled): static { + $this->serviceEnabled = $enabled; + return $this; + } + + public function getLocation(): null { + return null; + } + + public function getIdentity(): null { + return null; + } + + public function getAuxiliary(): array { + return []; + } + + // Collection operations - /** - * @inheritdoc - */ public function collectionList(string|int|null $location = null, ?IFilter $filter = null, ?ISort $sort = null): array { $entries = $this->metaStore->collectionList($this->serviceTenantId, $this->serviceUserId, $location, $filter, $sort); // cache collections @@ -197,45 +184,37 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $entries ?? []; } - /** - * @inheritdoc - */ public function collectionListFilter(): IFilter { return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); } - /** - * @inheritdoc - */ public function collectionListSort(): ISort { return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } - /** - * @inheritdoc - */ - public function collectionExtant(string|int|null $identifier): bool { - // null is root - if ($identifier === null) { - return true; + public function collectionExtant(string|int $location, string|int ...$identifiers): array { + $cached = []; + $toCheck = []; + foreach ($identifiers as $id) { + if (isset($this->serviceCollectionCache[$id])) { + $cached[$id] = true; + } else { + $toCheck[] = $id; + } } - // check cache first - if (isset($this->serviceCollectionCache[$identifier])) { - return true; + if (empty($toCheck)) { + return $cached; } - // check store - return $this->metaStore->collectionExtant($this->serviceTenantId, $this->serviceUserId, $identifier); + $fromStore = $this->metaStore->collectionExtant($this->serviceTenantId, $this->serviceUserId, ...$toCheck); + return array_merge($cached, $fromStore); } - /** - * @inheritdoc - */ - public function collectionFetch(string|int|null $identifier): ?INodeCollectionBase { + public function collectionFetch(string|int $identifier): ?CollectionResource { // null is root if ($identifier === null) { $identifier = self::ROOT_ID; } - // check cache first + // check cache first if (isset($this->serviceCollectionCache[$identifier])) { return $this->serviceCollectionCache[$identifier]; } @@ -247,31 +226,23 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi } return null; } - - // ==================== Collection Methods (IServiceCollectionMutable) ==================== - - /** - * @inheritdoc - */ - public function collectionFresh(): INodeCollectionMutable { - $collection = new NodeCollection(); + + public function collectionFresh(): CollectionResource { + $collection = new CollectionResource(); $collection->fromStore([ 'tid' => $this->serviceTenantId, 'uid' => $this->serviceUserId, + 'cid' => null, 'nid' => null, - 'pid' => null, - 'createdBy' => $this->serviceUserId, - 'createdOn' => (new \DateTimeImmutable())->format('c'), - 'owner' => $this->serviceUserId, - 'label' => '', + 'type' => NodeType::Collection->value, + 'properties' => [ + 'label' => '', + ], ]); return $collection; } - /** - * @inheritdoc - */ - public function collectionCreate(string|int|null $location, INodeCollectionMutable $collection, array $options = []): INodeCollectionBase { + public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionResource { // null is root if ($location === null) { $location = self::ROOT_ID; @@ -279,49 +250,57 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi // Create in meta store $node = $this->metaStore->collectionCreate($this->serviceTenantId, $this->serviceUserId, $location, $collection, $options); // cache collection - $this->serviceCollectionCache[$node->id()] = $node; + $this->serviceCollectionCache[$node->identifier()] = $node; return $node; } - /** - * @inheritdoc - */ - public function collectionModify(string|int $identifier, INodeCollectionMutable $collection): INodeCollectionBase { + public function collectionUpdate(string|int $identifier, CollectionMutableInterface $collection): CollectionResource { // Modify in meta store $node = $this->metaStore->collectionModify($this->serviceTenantId, $this->serviceUserId, $identifier, $collection); // update cache - $this->serviceCollectionCache[$node->id()] = $node; + $this->serviceCollectionCache[$node->identifier()] = $node; return $node; } - /** - * @inheritdoc - */ - public function collectionDestroy(string|int $identifier): bool { + public function collectionDelete(string|int $identifier, bool $force = false, bool $recursive = false): bool { // Protect root collection if ($identifier === self::ROOT_ID) { throw new InvalidParameterException("Cannot destroy root collection"); } - // Delete from meta store + // If not forcing and not recursive, ensure the collection is empty + if (!$force && !$recursive) { + $children = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, false); + if (!empty($children)) { + throw new InvalidParameterException("Collection is not empty: $identifier"); + } + } + + // Delete blob files for all entities within the collection tree + $descendants = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, true); + foreach ($descendants as $nodeId => $node) { + if ($node->isEntity()) { + $this->blobStore->blobDelete((string)$nodeId); + } + } + + // Delete from meta store (handles recursive collection/entity meta deletion internally) $result = $this->metaStore->collectionDestroy($this->serviceTenantId, $this->serviceUserId, $identifier); - + // remove from cache unset($this->serviceCollectionCache[$identifier]); return $result; } - /** - * @inheritdoc - */ - public function collectionCopy(string|int $identifier, string|int|null $location): INodeCollectionBase { + public function collectionCopy(string|int $identifier, string|int|null $location): CollectionResource { // Protect root collection if ($identifier === self::ROOT_ID) { throw new InvalidParameterException("Cannot copy root collection"); } // Verify collection exists - if (!$this->collectionExtant($identifier)) { + $extant = $this->collectionExtant($identifier, $identifier); + if (!($extant[$identifier] ?? false)) { throw new InvalidParameterException("Collection not found: $identifier"); } @@ -333,20 +312,18 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi $node = $this->metaStore->collectionCopy($this->serviceTenantId, $this->serviceUserId, $identifier, $location); // cache collection - $this->serviceCollectionCache[$node->id()] = $node; + $this->serviceCollectionCache[$node->identifier()] = $node; return $node; } - /** - * @inheritdoc - */ - public function collectionMove(string|int $identifier, string|int|null $location): INodeCollectionBase { + public function collectionMove(string|int $identifier, string|int|null $location): CollectionResource { // Protect root collection if ($identifier === self::ROOT_ID) { throw new InvalidParameterException("Cannot move root collection"); } // Verify collection exists - if (!$this->collectionExtant($identifier)) { + $extant = $this->collectionExtant($identifier, $identifier); + if (!($extant[$identifier] ?? false)) { throw new InvalidParameterException("Collection not found: $identifier"); } @@ -358,16 +335,13 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi $node = $this->metaStore->collectionMove($this->serviceTenantId, $this->serviceUserId, $identifier, $location); // update cache - $this->serviceCollectionCache[$node->id()] = $node; + $this->serviceCollectionCache[$node->identifier()] = $node; return $node; } - // ==================== Entity Methods (IServiceBase) ==================== + // Entity operations - /** - * @inheritdoc - */ - public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array { + public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; @@ -376,28 +350,19 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi $entries = $this->metaStore->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range); // cache entities foreach ($entries as $id => $entity) { - $this->serviceEntityCache[$id] = $entity; + $this->serviceEntityCache[$entity->identifier()] = $entity; } return $entries ?? []; } - /** - * @inheritdoc - */ public function entityListFilter(): IFilter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } - /** - * @inheritdoc - */ public function entityListSort(): ISort { return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []); } - /** - * @inheritdoc - */ public function entityListRange(RangeType $type): IRange { if ($type !== RangeType::TALLY) { throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not supported"); @@ -405,43 +370,6 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return new RangeTally(); } - /** - * @inheritdoc - */ - public function entityDelta(string|int|null $collection, string $signature, string $detail = 'ids'): array { - // null collection is root - if ($collection === null) { - $collection = self::ROOT_ID; - } - return $this->metaStore->entityDelta($this->serviceTenantId, $this->serviceUserId, $collection, $signature, $detail); - } - - /** - * @inheritdoc - */ - public function entityExtant(string|int|null $collection, string|int ...$identifiers): array { - // null collection is root - if ($collection === null) { - $collection = self::ROOT_ID; - } - - $result = []; - foreach ($identifiers as $id) { - // check cache first - if (isset($this->serviceEntityCache[$id])) { - $result[$id] = true; - continue; - } - // check store - $result[$id] = $this->metaStore->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $id); - } - - return $result; - } - - /** - * @inheritdoc - */ public function entityFetch(string|int|null $collection, string|int ...$identifiers): array { // null collection is root if ($collection === null) { @@ -472,162 +400,108 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $result; } - /** - * @inheritdoc - */ - public function entityRead(string|int|null $collection, string|int $identifier): ?string { + public function entityExtant(string|int|null $collection, string|int ...$identifiers): array { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; } - $entities = $this->entityFetch($collection, $identifier); - if (!isset($entities[$identifier])) { - return null; + $result = []; + foreach ($identifiers as $id) { + // check cache first + if (isset($this->serviceEntityCache[$id])) { + $result[$id] = true; + continue; + } + // check store + $result[$id] = $this->metaStore->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $id); } - return $this->blobStore->blobRead((string)$identifier); + + return $result; } - /** - * @inheritdoc - */ - public function entityReadStream(string|int|null $collection, string|int $identifier) { - // null collection is root - if ($collection === null) { - $collection = self::ROOT_ID; - } - - $entities = $this->entityFetch($collection, $identifier); - if (!isset($entities[$identifier])) { - return null; - } - - return $this->blobStore->blobReadStream((string)$identifier); - } - - /** - * @inheritdoc - */ - public function entityReadChunk(string|int|null $collection, string|int $identifier, int $offset, int $length): ?string { - // null collection is root - if ($collection === null) { - $collection = self::ROOT_ID; - } - - $entities = $this->entityFetch($collection, $identifier); - if (!isset($entities[$identifier])) { - return null; - } - - return $this->blobStore->blobReadChunk((string)$identifier, $offset, $length); - } - - // ==================== Entity Methods (IServiceEntityMutable) ==================== - - /** - * @inheritdoc - */ - public function entityFresh(): INodeEntityMutable { - $entity = new NodeEntity(); + public function entityFresh(): EntityResource { + $entity = new EntityResource(); $entity->fromStore([ 'tid' => $this->serviceTenantId, 'uid' => $this->serviceUserId, + 'cid' => null, 'nid' => null, - 'pid' => null, - 'createdBy' => $this->serviceUserId, - 'createdOn' => (new \DateTimeImmutable())->format('c'), - 'owner' => $this->serviceUserId, - 'label' => '', + 'created' => (new \DateTimeImmutable())->format('c'), + 'properties' => [], ]); return $entity; } - /** - * Build metadata array for an entity to store in .meta file - * - * @param INodeEntityBase $node The entity node - * @return array Metadata array - */ - protected function buildEntityMeta(INodeEntityBase $node): array { - return [ - 'nid' => $node->id(), - 'pid' => $node->in(), - 'label' => $node->getLabel(), - 'mime' => $node->getMime(), - 'format' => $node->getFormat(), - 'createdBy' => $node->createdBy(), - 'createdOn' => $node->createdOn()?->format('c'), - 'modifiedBy' => $node->modifiedBy(), - 'modifiedOn' => $node->modifiedOn()?->format('c'), - ]; - } - - /** - * @inheritdoc - */ - public function entityCreate(string|int|null $collection, INodeEntityMutable $entity, array $options = []): INodeEntityBase { + public function entityCreate(string|int $collection, EntityMutableInterface $entity, array $options = []): EntityResource { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; } // Create in meta store - $node = $this->metaStore->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $entity, $options); + $result = $this->metaStore->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $entity, $options); // Write meta file for recovery - $this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node)); + $this->blobStore->metaWrite((string)$result->identifier(), $this->buildEntityMeta($result)); // cache entity - $this->serviceEntityCache[$node->id()] = $node; - return $node; + $this->serviceEntityCache[$result->identifier()] = $result; + return $result; } - /** - * @inheritdoc - */ - public function entityModify(string|int|null $collection, string|int $identifier, INodeEntityMutable $entity): INodeEntityBase { + public function entityUpdate(string|int|null $collection, string|int $identifier, EntityMutableInterface $entity): EntityResource { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; } // Modify in meta store - $node = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $entity); + $result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $entity); // Update meta file for recovery - $this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node)); + $this->blobStore->metaWrite((string)$result->identifier(), $this->buildEntityMeta($result)); // update cache - $this->serviceEntityCache[$node->id()] = $node; - return $node; + $this->serviceEntityCache[$result->identifier()] = $result; + return $result; } - /** - * @inheritdoc - */ - public function entityDestroy(string|int|null $collection, string|int $identifier): bool { + public function entityDelete(string|int $collection, string|int $identifier): EntityResource { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; } - // Delete from meta store - $result = $this->metaStore->entityDestroy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - - // Delete file from filesystem - if ($result) { - $this->blobStore->blobDelete((string)$identifier); + /** @var EntityResource[] $entities */ + $entities = $this->entityFetch($collection, $identifier); + + if (!isset($entities[$identifier])) { + throw new InvalidParameterException("Entity not found: $identifier"); } - + + $entity = $entities[$identifier]; + + // Delete from blob store + $this->blobStore->blobDelete($entity->identifier()); + + // Delete from meta store + $this->metaStore->entityDestroy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); + // remove from cache - unset($this->serviceEntityCache[$identifier]); - return $result; + unset($this->serviceEntityCache[$entity->identifier()]); + return $entity; } - /** - * @inheritdoc - */ - public function entityCopy(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase { + public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta { + // null collection is root + if ($collection === null) { + $collection = self::ROOT_ID; + } + return new Delta(); // $this->metaStore->entityDelta($this->serviceTenantId, $this->serviceUserId, $collection, $signature, $detail); + } + + public function entityCopy(string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; @@ -649,21 +523,18 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi // Copy file content (new entity has new UUID = new file) $content = $this->blobStore->blobRead((string)$identifier); if ($content !== null) { - $this->blobStore->blobWrite((string)$node->id(), $content); + $this->blobStore->blobWrite((string)$node->identifier(), $content); } // Write meta file for recovery - $this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node)); + $this->blobStore->metaWrite((string)$node->identifier(), $this->buildEntityMeta($node)); // cache entity - $this->serviceEntityCache[$node->id()] = $node; + $this->serviceEntityCache[$node->identifier()] = $node; return $node; } - - /** - * @inheritdoc - */ - public function entityMove(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase { + + public function entityMove(string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; @@ -683,55 +554,93 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi $node = $this->metaStore->entityMove($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $destination); // Update meta file for recovery (parent changed) - $this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node)); + $this->blobStore->metaWrite((string)$node->identifier(), $this->buildEntityMeta($node)); // update cache - $this->serviceEntityCache[$node->id()] = $node; + $this->serviceEntityCache[$node->identifier()] = $node; return $node; } + + public function entityRead(string|int|null $collection, string|int $identifier): ?string { + // null collection is root + if ($collection === null) { + $collection = self::ROOT_ID; + } + + $entities = $this->entityFetch($collection, $identifier); + if (!isset($entities[$identifier])) { + return null; + } + return $this->blobStore->blobRead((string)$identifier); + } + + public function entityReadStream(string|int|null $collection, string|int $identifier) { + // null collection is root + if ($collection === null) { + $collection = self::ROOT_ID; + } + + $entities = $this->entityFetch($collection, $identifier); + if (!isset($entities[$identifier])) { + return null; + } + + return $this->blobStore->blobReadStream((string)$identifier); + } + + public function entityReadChunk(string|int|null $collection, string|int $identifier, int $offset, int $length): ?string { + // null collection is root + if ($collection === null) { + $collection = self::ROOT_ID; + } + + $entities = $this->entityFetch($collection, $identifier); + if (!isset($entities[$identifier])) { + return null; + } + + return $this->blobStore->blobReadChunk((string)$identifier, $offset, $length); + } - /** - * @inheritdoc - */ public function entityWrite(string|int|null $collection, string|int $identifier, string $data): int { // null collection is root if ($collection === null) { $collection = self::ROOT_ID; } - // Verify entity exists + // verify entity exists $extant = $this->entityExtant($collection, $identifier); if (!($extant[$identifier] ?? false)) { throw new InvalidParameterException("Entity not found: $identifier"); } - // Detect MIME type and format from content header + // detect MIME type and format from content header $signature = Signature::detect(substr($data, 0, Signature::HEADER_SIZE)); - $bytes = $this->blobStore->blobWrite((string)$identifier, $data); - if ($bytes === null) { + // Write blob data + $size = $this->blobStore->blobWrite((string)$identifier, $data); + if ($size === null) { throw new \RuntimeException("Failed to write to entity: $identifier"); } - // Update metadata (size, mime, and format) - $this->metaStore->entityUpdate($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [ - 'size' => $bytes, - 'mime' => $signature['mime'], - 'format' => $signature['format'], - ]); + // Update meta store + $result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [ + 'properties' => [ + 'size' => $size, + 'mime' => $signature['mime'], + 'format' => $signature['format'], + ], + ], true); - // Update meta file - $entities = $this->entityFetch($collection, $identifier); - if (isset($entities[$identifier])) { - $this->blobStore->metaWrite((string)$identifier, $this->buildEntityMeta($entities[$identifier])); - } + // Update meta blob + $this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result)); - return $bytes; - } + // update cache + $this->serviceEntityCache[$result->identifier()] = $result; - /** - * @inheritdoc - */ + return $size; + } + public function entityWriteStream(string|int|null $collection, string|int $identifier) { // null collection is root if ($collection === null) { @@ -743,13 +652,30 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi if (!($extant[$identifier] ?? false)) { throw new InvalidParameterException("Entity not found: $identifier"); } + + // write blob stream + $stream = $this->blobStore->blobWriteStream((string)$identifier); + if ($stream === null) { + throw new \RuntimeException("Failed to open write stream for entity: $identifier"); + } + + // update meta store + $size = $this->blobStore->blobSize((string)$identifier) ?? 0; + $result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [ + 'properties' => [ + 'size' => $size, + ], + ], true); + + // update meta blob + $this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result)); - return $this->blobStore->blobWriteStream((string)$identifier); + // update cache + $this->serviceEntityCache[$result->identifier()] = $result; + + return $stream; } - /** - * @inheritdoc - */ public function entityWriteChunk(string|int|null $collection, string|int $identifier, int $offset, string $data): int { // null collection is root if ($collection === null) { @@ -773,36 +699,26 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi throw new \RuntimeException("Failed to write chunk to entity: $identifier"); } - // Build update attributes - $updates = []; + // update meta store + $size = $this->blobStore->blobSize((string)$identifier) ?? 0; + $result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [ + 'properties' => [ + 'size' => $size, + 'mime' => $signature['mime'] ?? null, + 'format' => $signature['format'] ?? null, + ] + ], true); + + // Update meta blob + $this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result)); - // Update size metadata - $newSize = $this->blobStore->blobSize((string)$identifier); - if ($newSize !== null) { - $updates['size'] = $newSize; - } - - // Update mime and format if detected (first chunk) - if ($signature !== null) { - $updates['mime'] = $signature['mime']; - $updates['format'] = $signature['format']; - } - - // Perform single database update - if (!empty($updates)) { - $this->metaStore->entityUpdate($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $updates); - } - - // Update meta file - $entities = $this->entityFetch($collection, $identifier); - if (isset($entities[$identifier])) { - $this->blobStore->metaWrite((string)$identifier, $this->buildEntityMeta($entities[$identifier])); - } + // update cache + $this->serviceEntityCache[$result->identifier()] = $result; return $bytes; } - // ==================== Node Methods (IServiceBase) ==================== + // Node operations /** * @inheritdoc @@ -819,14 +735,14 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi * @inheritdoc */ public function nodeListFilter(): IFilter { - return new Filter($this->serviceAbilities[self::CAPABILITY_NODE_LIST_FILTER] ?? []); + return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); } /** * @inheritdoc */ public function nodeListSort(): ISort { - return new Sort($this->serviceAbilities[self::CAPABILITY_NODE_LIST_SORT] ?? []); + return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } /** @@ -846,4 +762,24 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $this->metaStore->nodeDelta($this->serviceTenantId, $this->serviceUserId, $location, $signature, $recursive, $detail); } + /** + * Build metadata array for an entity to store in .meta file + * + * @param EntityResource $node The entity node + * @return array Metadata array + */ + protected function buildEntityMeta(EntityResource $node): array { + return [ + 'tid' => $this->serviceTenantId, + 'uid' => $this->serviceUserId, + 'cid' => $node->collection(), + 'nid' => $node->identifier(), + 'created' => $node->created()?->format('c'), + 'modified' => $node->modified()?->format('c'), + 'size' => $node->getProperties()->size(), + 'label' => $node->getProperties()->getLabel(), + 'mime' => $node->getProperties()->getMime(), + 'format' => $node->getProperties()->getFormat(), + ]; + } } diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index 5a56100..53273f1 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -7,20 +7,21 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\FileProviderLocal\Providers; +namespace KTXM\ProviderLocalDocuments\Providers; use DI\Attribute\Inject; use Psr\Container\ContainerInterface; -use KTXF\Files\Provider\IProviderBase; -use KTXF\Files\Service\IServiceBase; -use KTXF\Resource\Provider\ProviderInterface; -use KTXM\FileProviderLocal\Providers\Personal\PersonalService; +use KTXF\Resource\Documents\Provider\ProviderBaseInterface; +use KTXF\Resource\Documents\Service\ServiceBaseInterface; +use KTXM\ProviderLocalDocuments\Module; +use KTXM\ProviderLocalDocuments\Providers\Personal\PersonalService; -class Provider implements IProviderBase, ProviderInterface { +class Provider implements ProviderBaseInterface { - protected const PROVIDER_ID = 'default'; - protected const PROVIDER_LABEL = 'Default File Provider'; - protected const PROVIDER_DESCRIPTION = 'Provides local file storage'; + public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; + public const PROVIDER_IDENTIFIER = 'default'; + protected const PROVIDER_LABEL = Module::MODULE_LABEL; + protected const PROVIDER_DESCRIPTION = Module::MODULE_DESCRIPTION; protected const PROVIDER_ICON = 'folder'; protected string $storeLocation = '/tmp/ktrix'; @@ -30,8 +31,6 @@ class Provider implements IProviderBase, ProviderInterface { self::CAPABILITY_SERVICE_FETCH => true, self::CAPABILITY_SERVICE_EXTANT => true, ]; - private ?array $servicesCache = []; - public function __construct( private readonly ContainerInterface $container, #[Inject('rootDir')] private readonly string $rootDir, @@ -39,176 +38,94 @@ class Provider implements IProviderBase, ProviderInterface { $this->storeLocation = $this->rootDir . '/storage/'; } - /** - * @inheritDoc - */ - public function type(): string { - return ProviderInterface::TYPE_FILES; - } + public function jsonSerialize(): array + { + return [ + self::JSON_PROPERTY_TYPE => self::JSON_TYPE, + self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, + self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, + self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, + ]; + } - /** - * @inheritDoc - */ - public function identifier(): string { - return self::PROVIDER_ID; - } + public function jsonDeserialize(array|string $data): static + { + return $this; + } - /** - * @inheritDoc - */ - public function description(): string { - return self::PROVIDER_DESCRIPTION; - } + public function type(): string + { + return self::TYPE_CHRONO; + } - /** - * @inheritDoc - */ - public function icon(): string { - return self::PROVIDER_ICON; - } + public function identifier(): string + { + return self::PROVIDER_IDENTIFIER; + } - public function jsonSerialize(): mixed { - return $this->toJson(); - } + public function label(): string + { + return self::PROVIDER_LABEL; + } - public function toJson(): array { - return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_ID => self::PROVIDER_ID, - self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, - self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, - ]; - } + public function description(): string + { + return self::PROVIDER_DESCRIPTION; + } - /** - * Confirms if specific capability is supported - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function capable(string $value): bool { - if (isset($this->providerAbilities[$value])) { - return (bool)$this->providerAbilities[$value]; - } - return false; - } + public function icon(): string + { + return self::PROVIDER_ICON; + } - /** - * Lists all supported capabilities - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function capabilities(): array { - return $this->providerAbilities; - } + public function capable(string $value): bool + { + return !empty($this->providerAbilities[$value]); + } - /** - * An arbitrary unique text string identifying this provider - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function id(): string { - return self::PROVIDER_ID; - } + public function capabilities(): array + { + return $this->providerAbilities; + } - /** - * The localized human friendly name of this provider - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function label(): string { - return self::PROVIDER_LABEL; - } - - /** - * Retrieve collection of services for a specific user - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function serviceList(string $tenantId, string $userId, array $filter = []): array { - // if no filter is provided, return all services - if ($filter === []) { - $filter = ['personal', 'shared']; - } - // check if services are cached - if (in_array('personal', $filter, true) && !isset($this->servicesCache[$userId]['personal'])) { - $this->servicesCache[$userId]['personal'] = $this->serviceInstancePersonal($tenantId, $userId); - } - /* - if (in_array('shared', $filter, true) && !isset($this->servicesCache[$userId]['shared'])) { - $this->servicesCache[$userId]['shared'] = $this->serviceInstanceShared($tenantId, $userId); - } - */ - // return requested services - return array_intersect_key($this->servicesCache[$userId],array_flip($filter)); - } - - /** - * construct service object instance - * - * @since 1.0.0 - * - * @return PersonalService blank service instance - */ protected function serviceInstancePersonal(string $tenantId, string $userId): PersonalService { $service = $this->container->get(PersonalService::class); - $service->init($tenantId, $userId, $this->storeLocation . "$tenantId/$userId"); + $service->initialize($tenantId, $userId, $this->storeLocation . "$tenantId/$userId"); return $service; } - /** - * Determine if any services are configured for a specific user - * - * @since 1.0.0 - * - * @inheritdoc - */ + public function serviceList(string $tenantId, string $userId, array $filter = []): array { + // if no filter is provided, return all services + if ($filter === []) { + $filter = ['personal']; + } + // build services list + $services = []; + if (in_array('personal', $filter, true)) { + $services['personal'] = $this->serviceInstancePersonal($tenantId, $userId); + } + return $services; + } + + public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?ServiceBaseInterface { + + if ($identifier === 'personal') { + return $this->serviceInstancePersonal($tenantId, $userId); + } + + return null; + + } + public function serviceExtant(string $tenantId, string $userId, int|string ...$identifiers): array { $data = []; foreach ($identifiers as $id) { $data[$id] = match ($id) { 'personal' => true, - //'shared' => true, default => false, }; } return $data; } - /** - * Retrieve a service with a specific identifier - * - * @since 1.0.0 - * - * @inheritdoc - */ - public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?IServiceBase { - - // check if services are cached - if (isset($this->servicesCache[$userId][$identifier])) { - return $this->servicesCache[$userId][$identifier]; - } - // convert to service object - if ($identifier === 'personal') { - $this->servicesCache[$userId][$identifier] = $this->serviceInstancePersonal($tenantId, $userId); - } - /* - if ($identifier === 'shared') { - $this->servicesCache[$userId][$identifier] = $this->serviceInstanceShared($tenantId, $userId); - } - */ - - return $this->servicesCache[$userId][$identifier]; - - } - } diff --git a/lib/Store/BlobStore.php b/lib/Store/BlobStore.php index 46d716c..ff66355 100644 --- a/lib/Store/BlobStore.php +++ b/lib/Store/BlobStore.php @@ -7,7 +7,7 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\FileProviderLocal\Store; +namespace KTXM\ProviderLocalDocuments\Store; use RuntimeException; diff --git a/lib/Store/MetaStore.php b/lib/Store/MetaStore.php index 53e41b7..4beacad 100644 --- a/lib/Store/MetaStore.php +++ b/lib/Store/MetaStore.php @@ -7,11 +7,9 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\FileProviderLocal\Store; +namespace KTXM\ProviderLocalDocuments\Store; use KTXC\Db\DataStore; -use KTXF\Files\Node\INodeCollectionMutable; -use KTXF\Files\Node\INodeEntityMutable; use KTXF\Files\Node\NodeType; use KTXF\Resource\Filter\Filter; use KTXF\Resource\Filter\FilterComparisonOperator; @@ -21,13 +19,15 @@ use KTXF\Resource\Range\IRangeTally; use KTXF\Resource\Range\RangeType; use KTXF\Resource\Sort\Sort; use KTXF\Utile\UUID; -use KTXM\FileProviderLocal\Providers\Personal\NodeCollection; -use KTXM\FileProviderLocal\Providers\Personal\NodeEntity; +use KTXM\ProviderLocalDocuments\Providers\Personal\CollectionResource; +use KTXM\ProviderLocalDocuments\Providers\Personal\EntityResource; +use KTXM\ProviderLocalDocuments\Providers\Personal\NodeCollection; class MetaStore { - protected string $_NodeTable = 'file_provider_local_node'; - protected string $_ChronicleTable = 'file_provider_local_chronicle'; + private const ROOT_COLLECTION_ID = '00000000-0000-0000-0000-000000000000'; + protected string $_NodeTable = 'provider_local_documents_node'; + protected string $_ChronicleTable = 'provider_local_documents_chronicle'; protected array $_CollectionFilterAttributeMap = [ 'id' => 'nid', @@ -137,7 +137,7 @@ class MetaStore { $query = [ 'tid' => $tenantId, 'uid' => $userId, - 'pid' => $location, + 'cid' => empty($location) ? self::ROOT_COLLECTION_ID : $location, 'type' => NodeType::Collection->value, ]; @@ -157,8 +157,8 @@ class MetaStore { $cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options); $list = []; foreach ($cursor as $entry) { - $node = (new NodeCollection())->fromStore($entry); - $list[$node->id()] = $node; + $node = $this->collectionFresh()->fromStore($entry); + $list[$node->identifier()] = $node; } return $list; } @@ -166,14 +166,22 @@ class MetaStore { /** * Check if a collection exists */ - public function collectionExtant(string $tenantId, string $userId, string|int $identifier): bool { - $cursor = $this->_store->selectCollection($this->_NodeTable)->findOne([ + public function collectionExtant(string $tenantId, string $userId, string|int ...$identifiers): array { + $cursor = $this->_store->selectCollection($this->_NodeTable)->find([ 'tid' => $tenantId, 'uid' => $userId, - 'nid' => $identifier, + 'nid' => ['$in' => $identifiers], 'type' => NodeType::Collection->value, - ]); - return $cursor !== null; + ], ['projection' => ['nid' => 1]]); + $found = []; + foreach ($cursor as $entry) { + $found[(string)$entry['nid']] = true; + } + $result = []; + foreach ($identifiers as $id) { + $result[$id] = isset($found[(string)$id]); + } + return $result; } /** @@ -189,47 +197,50 @@ class MetaStore { $list = []; foreach ($cursor as $entry) { - $node = (new NodeCollection())->fromStore($entry); - $list[$node->id()] = $node; + $node = $this->collectionFresh()->fromStore($entry); + $list[$node->identifier()] = $node; } return $list; } + /** + * Fresh collection instance + */ + protected function collectionFresh(): CollectionResource { + return new CollectionResource(); + } + /** * Create a collection */ - public function collectionCreate(string $tenantId, string $userId, string|int|null $location, INodeCollectionMutable $collection, array $options = []): NodeCollection { + public function collectionCreate(string $tenantId, string $userId, string|int|null $location, CollectionResource $collection, array $options = []): CollectionResource { $data = [ 'tid' => $tenantId, 'uid' => $userId, + 'cid' => empty($location) ? self::ROOT_COLLECTION_ID : $location, 'nid' => UUID::v4(), - 'pid' => $location, 'type' => NodeType::Collection->value, - 'createdBy' => $userId, - 'createdOn' => date('c'), - 'modifiedBy' => $userId, - 'modifiedOn' => date('c'), - 'owner' => $userId, - 'label' => $collection->getLabel(), + 'created' => date('c'), + 'modified' => date('c'), + 'properties' => $collection->getProperties()->toStore(), ]; - $data['signature'] = md5(json_encode([$data['label'], $data['modifiedOn']])); + $data['signature'] = time(); $this->_store->selectCollection($this->_NodeTable)->insertOne($data); $this->chronicleNode($tenantId, $userId, $data['nid'], 1); - return (new NodeCollection())->fromStore($data); + return $this->collectionFresh()->fromStore($data); } /** * Modify a collection */ - public function collectionModify(string $tenantId, string $userId, string|int $identifier, INodeCollectionMutable $collection): NodeCollection { + public function collectionModify(string $tenantId, string $userId, string|int $identifier, CollectionResource $collection): CollectionResource { $data = [ - 'modifiedOn' => date('c'), - 'modifiedBy' => $userId, - 'label' => $collection->getLabel(), + 'modified' => date('c'), + 'properties' => $collection->getProperties()->toStore(), ]; - $data['signature'] = md5(json_encode([$data['label'], $data['modifiedOn']])); + $data['signature'] = time(); $this->_store->selectCollection($this->_NodeTable)->updateOne( ['tid' => $tenantId, 'uid' => $userId, 'nid' => $identifier], @@ -273,11 +284,10 @@ class MetaStore { /** * Move a collection */ - public function collectionMove(string $tenantId, string $userId, string|int $identifier, string|int|null $location): NodeCollection { + public function collectionMove(string $tenantId, string $userId, string|int $identifier, string|int|null $location): CollectionResource { $data = [ - 'pid' => $location, - 'modifiedBy' => $userId, - 'modifiedOn' => date('c'), + 'cid' => empty($location) ? self::ROOT_COLLECTION_ID : $location, + 'modified' => date('c'), ]; $this->_store->selectCollection($this->_NodeTable)->updateOne( @@ -293,14 +303,14 @@ class MetaStore { /** * Copy a collection */ - public function collectionCopy(string $tenantId, string $userId, string|int $identifier, string|int|null $location): NodeCollection { + public function collectionCopy(string $tenantId, string $userId, string|int $identifier, string|int|null $location): CollectionResource { $collections = $this->collectionFetch($tenantId, $userId, $identifier); if (!isset($collections[$identifier])) { throw new \RuntimeException("Collection not found: $identifier"); } $source = $collections[$identifier]; - $newCollection = new NodeCollection(); + $newCollection = $this->collectionFresh(); $newCollection->setLabel($source->getLabel()); $newNode = $this->collectionCreate($tenantId, $userId, $location, $newCollection); @@ -309,9 +319,9 @@ class MetaStore { $children = $this->nodeList($tenantId, $userId, $identifier, false); foreach ($children as $childId => $child) { if ($child->isCollection()) { - $this->collectionCopy($tenantId, $userId, $childId, $newNode->id()); + $this->collectionCopy($tenantId, $userId, $childId, $newNode->identifier()); } else { - $this->entityCopy($tenantId, $userId, $identifier, $childId, $newNode->id()); + $this->entityCopy($tenantId, $userId, $identifier, $childId, $newNode->identifier()); } } @@ -320,6 +330,13 @@ class MetaStore { // ========== Entity Operations ========== + /** + * Fresh entity instance + */ + protected function entityFresh(): EntityResource { + return new EntityResource(); + } + /** * List entities in a collection */ @@ -327,7 +344,7 @@ class MetaStore { $query = [ 'tid' => $tenantId, 'uid' => $userId, - 'pid' => $collection, + 'cid' => $collection, 'type' => NodeType::Entity->value, ]; @@ -352,8 +369,8 @@ class MetaStore { $cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options); $list = []; foreach ($cursor as $entry) { - $node = (new NodeEntity())->fromStore($entry); - $list[$node->id()] = $node; + $node = $this->entityFresh()->fromStore($entry); + $list[$node->identifier()] = $node; } return $list; } @@ -365,7 +382,7 @@ class MetaStore { $cursor = $this->_store->selectCollection($this->_NodeTable)->findOne([ 'tid' => $tenantId, 'uid' => $userId, - 'pid' => $collection, + 'cid' => $collection, 'nid' => $identifier, 'type' => NodeType::Entity->value, ]); @@ -379,15 +396,15 @@ class MetaStore { $cursor = $this->_store->selectCollection($this->_NodeTable)->find([ 'tid' => $tenantId, 'uid' => $userId, - 'pid' => $collection, + 'cid' => $collection, 'nid' => ['$in' => $identifiers], 'type' => NodeType::Entity->value, ]); $list = []; foreach ($cursor as $entry) { - $node = (new NodeEntity())->fromStore($entry); - $list[$node->id()] = $node; + $node = $this->entityFresh()->fromStore($entry); + $list[$node->identifier()] = $node; } return $list; } @@ -395,48 +412,52 @@ class MetaStore { /** * Create an entity */ - public function entityCreate(string $tenantId, string $userId, string|int|null $collection, INodeEntityMutable $entity, array $options = []): NodeEntity { - $data = [ + public function entityCreate(string $tenantId, string $userId, string|int|null $collection, EntityResource $entity, array $options = []): EntityResource { + + $entity = [ 'tid' => $tenantId, 'uid' => $userId, + 'cid' => empty($collection) ? self::ROOT_COLLECTION_ID : $collection, 'nid' => UUID::v4(), - 'pid' => $collection, 'type' => NodeType::Entity->value, - 'createdOn' => date('c'), - 'createdBy' => $userId, - 'modifiedOn' => date('c'), - 'modifiedBy' => $userId, - 'size' => 0, - 'mime' => $entity->getMime(), - 'format' => $entity->getFormat(), - 'encoding' => $entity->getEncoding(), - 'label' => $entity->getLabel(), + 'created' => date('c'), + 'modified' => date('c'), + 'properties' => $entity->getProperties()->toStore(), ]; - $data['signature'] = md5(json_encode([$data['label'], $data['size'], $data['mime'], $data['modifiedOn']])); + $entity['signature'] = time(); - $this->_store->selectCollection($this->_NodeTable)->insertOne($data); - $this->chronicleNode($tenantId, $userId, $data['nid'], 1); + $this->_store->selectCollection($this->_NodeTable)->insertOne($entity); + $this->chronicleNode($tenantId, $userId, $entity['nid'], 1); - return (new NodeEntity())->fromStore($data); + return $this->entityFresh()->fromStore($entity); } /** * Modify an entity */ - public function entityModify(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, INodeEntityMutable $entity): NodeEntity { - $data = [ - 'label' => $entity->getLabel(), - 'mime' => $entity->getMime(), - 'format' => $entity->getFormat(), - 'encoding' => $entity->getEncoding(), - 'modifiedOn' => date('c'), - 'modifiedBy' => $userId, - ]; - $data['signature'] = md5(json_encode([$data['label'], $data['mime'], $data['modifiedOn']])); + public function entityModify(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, EntityResource|array $data, bool $partial = false): EntityResource { + $attributes = []; + + if ($data instanceof EntityResource) { + $data = $data->getProperties()->toStore(); + } + + if ($partial) { + foreach ($data['properties'] ?? [] as $key => $value) { + if ($value !== null) { + $attributes["properties.$key"] = $value; + } + } + } else { + $attributes['properties'] = $data['properties'] ?? []; + } + + $attributes['modified'] = date('c'); + $attributes['signature'] = time(); $this->_store->selectCollection($this->_NodeTable)->updateOne( - ['tid' => $tenantId, 'uid' => $userId, 'nid' => $identifier], - ['$set' => $data] + ['tid' => $tenantId, 'uid' => $userId, 'cid' => $collection, 'nid' => $identifier], + ['$set' => $attributes] ); $this->chronicleNode($tenantId, $userId, $identifier, 2); @@ -444,58 +465,31 @@ class MetaStore { return $entities[$identifier]; } - /** - * Update entity attributes - * - * Supported attributes: size, format, mime, encoding, label - * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string|int $collection collection identifier - * @param string|int $identifier entity identifier - * @param array $attributes key-value pairs of attributes to update - */ - public function entityUpdate(string $tenantId, string $userId, string|int $collection, string|int $identifier, array $attributes): void { - // Filter to allowed attributes only - $allowed = ['size', 'format', 'mime', 'encoding', 'label']; - $data = array_intersect_key($attributes, array_flip($allowed)); - - if (empty($data)) { - return; - } - - // Always update modification timestamp - $data['modifiedOn'] = date('c'); - - $this->_store->selectCollection($this->_NodeTable)->updateOne( - ['tid' => $tenantId, 'uid' => $userId, 'nid' => $identifier], - ['$set' => $data] - ); - $this->chronicleNode($tenantId, $userId, $identifier, 2); - } - /** * Destroy an entity */ public function entityDestroy(string $tenantId, string $userId, string|int|null $collection, string|int $identifier): bool { + $result = $this->_store->selectCollection($this->_NodeTable)->deleteOne([ 'tid' => $tenantId, 'uid' => $userId, + 'cid' => $collection, 'nid' => $identifier ]); - + if ($result->getDeletedCount() === 1) { $this->chronicleNode($tenantId, $userId, $identifier, 3); return true; } + return false; } /** * Move an entity to another collection */ - public function entityMove(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, string|int|null $destination): NodeEntity { + public function entityMove(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource { $data = [ 'pid' => $destination, 'modifiedOn' => date('c'), @@ -503,7 +497,7 @@ class MetaStore { ]; $this->_store->selectCollection($this->_NodeTable)->updateOne( - ['tid' => $tenantId, 'uid' => $userId, 'nid' => $identifier], + ['tid' => $tenantId, 'uid' => $userId, 'nid' => $identifier, 'cid' => $collection], ['$set' => $data] ); $this->chronicleNode($tenantId, $userId, $identifier, 2); @@ -515,14 +509,14 @@ class MetaStore { /** * Copy an entity */ - public function entityCopy(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, string|int|null $destination): NodeEntity { + public function entityCopy(string $tenantId, string $userId, string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource { $entities = $this->entityFetch($tenantId, $userId, $collection, $identifier); if (!isset($entities[$identifier])) { throw new \RuntimeException("Entity not found: $identifier"); } $source = $entities[$identifier]; - $newEntity = new NodeEntity(); + $newEntity = new EntityResource(); $newEntity->setLabel($source->getLabel()); $newEntity->setMime($source->getMime()); $newEntity->setFormat($source->getFormat()); @@ -588,7 +582,7 @@ class MetaStore { if ($nodeType === NodeType::Collection->value) { $node = (new NodeCollection())->fromStore($entry); } else { - $node = (new NodeEntity())->fromStore($entry); + $node = (new EntityResource())->fromStore($entry); } $list[$node->id()] = $node; } @@ -737,7 +731,7 @@ class MetaStore { if ($nodeType === NodeType::Collection->value) { $node = (new NodeCollection())->fromStore($entry); } else { - $node = (new NodeEntity())->fromStore($entry); + $node = (new EntityResource())->fromStore($entry); } $list[$node->id()] = $node; }