From 80434e54769cbe244b297c9486b090183d5ef643 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Wed, 25 Feb 2026 15:34:19 -0500 Subject: [PATCH] refactor: standardize design Signed-off-by: Sebastian Krupinski --- composer.json | 6 +- composer.lock | 23 + lib/Module.php | 6 +- lib/Providers/Personal/Collection.php | 260 -------- .../Personal/CollectionProperties.php | 67 ++ lib/Providers/Personal/CollectionResource.php | 101 +++ lib/Providers/Personal/Entity.php | 162 ----- lib/Providers/Personal/EntityProperties.php | 50 ++ lib/Providers/Personal/EntityResource.php | 93 +++ lib/Providers/Personal/PersonalService.php | 241 +++++--- lib/Providers/Provider.php | 124 ++-- lib/Providers/Shared/Collection.php | 342 ---------- lib/Providers/Shared/SharedService.php | 583 ------------------ lib/Store/Personal/Store.php | 159 +++-- 14 files changed, 622 insertions(+), 1595 deletions(-) create mode 100644 composer.lock delete mode 100644 lib/Providers/Personal/Collection.php create mode 100644 lib/Providers/Personal/CollectionProperties.php create mode 100644 lib/Providers/Personal/CollectionResource.php delete mode 100644 lib/Providers/Personal/Entity.php create mode 100644 lib/Providers/Personal/EntityProperties.php create mode 100644 lib/Providers/Personal/EntityResource.php delete mode 100644 lib/Providers/Shared/Collection.php delete mode 100644 lib/Providers/Shared/SharedService.php diff --git a/composer.json b/composer.json index 5d4105d..862452e 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "ktrix/people-provider-local", + "name": "ktrix/provider-local-people", "type": "project", "authors": [ { @@ -12,7 +12,7 @@ "platform": { "php": "8.2" }, - "autoloader-suffix": "PeopleProviderLocal", + "autoloader-suffix": "ProviderLocalPeople", "vendor-dir": "lib/vendor" }, "require": { @@ -20,7 +20,7 @@ }, "autoload": { "psr-4": { - "KTXM\\PeopleProviderLocal\\": "lib/" + "KTXM\\ProviderLocalPeople\\": "lib/" } } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..ccefaae --- /dev/null +++ b/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "24ffd3b8e64daaa4c2e9a40788312244", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2 <=8.5" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.2" + }, + "plugin-api-version": "2.3.0" +} diff --git a/lib/Module.php b/lib/Module.php index 2f5fe59..0723249 100644 --- a/lib/Module.php +++ b/lib/Module.php @@ -1,12 +1,12 @@ $this->handle(), - 'namespace' => 'PeopleProviderLocal', + 'namespace' => 'ProviderLocalPeople', 'version' => $this->version(), 'label' => $this->label(), 'author' => $this->author(), diff --git a/lib/Providers/Personal/Collection.php b/lib/Providers/Personal/Collection.php deleted file mode 100644 index 4e558b6..0000000 --- a/lib/Providers/Personal/Collection.php +++ /dev/null @@ -1,260 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\PeopleProviderLocal\Providers\Personal; - -use KTXF\People\Collection\CollectionContent; -use KTXF\People\Collection\CollectionPermissions; -use KTXF\People\Collection\CollectionRoles; -use KTXF\People\Collection\ICollectionMutable; - -class Collection implements ICollectionMutable { - - private ?string $userId = null; - private string $providerId = 'default'; - private string $serviceId = 'personal'; - private ?string $collectionId = null; - private ?string $collectionUuid = null; - private ?string $collectionLabel = null; - private ?string $collectionDescription = null; - private ?int $collectionPriority = null; - private ?bool $collectionVisibility = null; - private ?string $collectionColor = null; - private ?string $collectionCreatedOn = null; - private ?string $collectionModifiedOn = null; - private bool $collectionEnabled = true; - private ?string $collectionSignature = null; - private array $collectionPermissions = [ - CollectionPermissions::View->value => true, - CollectionPermissions::Create->value => true, - CollectionPermissions::Modify->value => true, - CollectionPermissions::Destroy->value => true, - CollectionPermissions::Share->value => true, - ]; - private array $collectionAttributes = [ - 'roles' => [ - CollectionRoles::Individual->value => true, - ], - 'contents' => [ - CollectionContent::Individual->value => true, - CollectionContent::Organization->value => true, - CollectionContent::Group->value => true, - ], - ]; - - public function jsonSerialize(): mixed { - return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_PROVIDER => $this->providerId, - self::JSON_PROPERTY_SERVICE => $this->serviceId, - self::JSON_PROPERTY_IN => null, - self::JSON_PROPERTY_ID => $this->collectionId, - self::JSON_PROPERTY_LABEL => $this->collectionLabel, - self::JSON_PROPERTY_DESCRIPTION => $this->collectionDescription, - self::JSON_PROPERTY_PRIORITY => $this->collectionPriority, - self::JSON_PROPERTY_VISIBILITY => $this->collectionVisibility, - self::JSON_PROPERTY_COLOR => $this->collectionColor, - self::JSON_PROPERTY_CREATED => $this->collectionCreatedOn, - self::JSON_PROPERTY_MODIFIED => $this->collectionModifiedOn, - self::JSON_PROPERTY_ENABLED => $this->collectionEnabled, - self::JSON_PROPERTY_SIGNATURE => $this->collectionSignature, - self::JSON_PROPERTY_PERMISSIONS => [$this->userId => $this->collectionPermissions], - self::JSON_PROPERTY_ROLES => $this->collectionAttributes['roles'] ?? [], - self::JSON_PROPERTY_CONTENTS => $this->collectionAttributes['contents'] ?? [], - ]; - } - - public function jsonDeserialize(array|string $data): static - { - if (is_string($data)) { - $data = json_decode($data, true); - } - - $this->collectionId = $data[self::JSON_PROPERTY_ID] ?? null; - $this->collectionLabel = $data[self::JSON_PROPERTY_LABEL] ?? null; - $this->collectionDescription = $data[self::JSON_PROPERTY_DESCRIPTION] ?? null; - $this->collectionPriority = $data[self::JSON_PROPERTY_PRIORITY] ?? null; - $this->collectionVisibility = $data[self::JSON_PROPERTY_VISIBILITY] ?? null; - $this->collectionColor = $data[self::JSON_PROPERTY_COLOR] ?? null; - $this->collectionCreatedOn = $data[self::JSON_PROPERTY_CREATED] ?? null; - $this->collectionModifiedOn = $data[self::JSON_PROPERTY_MODIFIED] ?? null; - $this->collectionEnabled = $data[self::JSON_PROPERTY_ENABLED] ?? true; - $this->collectionSignature = $data[self::JSON_PROPERTY_SIGNATURE] ?? null; - - return $this; - } - - public function fromStore(array|object $data): self - { - // Convert object to array if needed - if (is_object($data)) { - $data = (array) $data; - } - - // extract properties - if (isset($data['cid'])) { - $this->collectionId = $data['cid']; - } elseif (isset($data['_id'])) { - if (is_object($data['_id']) && method_exists($data['_id'], '__toString')) { - $this->collectionId = (string) $data['_id']; - } elseif (is_array($data['_id']) && isset($data['_id']['$oid'])) { - $this->collectionId = $data['_id']['$oid']; - } else { - $this->collectionId = (string) $data['_id']; - } - } - $this->userId = $data['uid'] ?? null; - $this->collectionLabel = $data['label'] ?? null; - $this->collectionDescription = $data['description'] ?? null; - $this->collectionColor = $data['color'] ?? null; - $this->collectionCreatedOn = $data['created'] ?? null; - $this->collectionModifiedOn = $data['modified'] ?? null; - $this->collectionEnabled = $data['enabled'] ?? true; - $this->collectionSignature = isset($data['signature']) ? md5((string)$data['signature']) : null; - // Handle BSON array if present - if (isset($data['tags'])) { - if (is_object($data['tags']) && method_exists($data['tags'], 'bsonSerialize')) { - $tags = $data['tags']->bsonSerialize(); - } else { - $tags = $data['tags'] ?? []; - } - } - - return $this; - } - - public function toStore(): array - { - $data = [ - 'uid' => $this->userId, - 'uuid' => $this->collectionUuid, - 'label' => $this->collectionLabel, - 'description' => $this->collectionDescription, - 'color' => $this->collectionColor, - 'created' => $this->collectionCreatedOn, - 'modified' => $this->collectionModifiedOn, - 'signature' => $this->collectionSignature, - 'enabled' => $this->collectionEnabled, - ]; - - // Only include _id if it exists (for updates) - if ($this->collectionId !== null) { - $data['_id'] = $this->collectionId; - } - - return $data; - } - - public function in(): null { - return null; - } - - public function id(): string { - return $this->collectionId; - } - - public function created(): ?\DateTimeImmutable { - return $this->collectionCreatedOn ? new \DateTimeImmutable($this->collectionCreatedOn) : null; - } - - public function modified(): ?\DateTimeImmutable { - return $this->collectionModifiedOn ? new \DateTimeImmutable($this->collectionModifiedOn) : null; - } - - public function attributes(): array { - return $this->collectionAttributes; - } - - public function uuid(): string { - return $this->collectionUuid; - } - - public function signature(): ?string { - return $this->collectionSignature; - } - - public function roles(): array { - return $this->collectionAttributes['roles'] ?? []; - } - - public function role(CollectionRoles $role): bool { - return $this->collectionAttributes['roles'][$role->value] ?? false; - } - - public function contents(): array { - return $this->collectionAttributes['content'] ?? []; - } - - public function contains(CollectionContent $content): bool { - return $this->collectionAttributes['content'][$content->value] ?? false; - } - - public function getEnabled(): bool { - return (bool)$this->collectionEnabled; - } - - public function setEnabled(bool $value): self { - $this->collectionEnabled = $value; - return $this; - } - - public function getPermissions(): array { - return [$this->userId => $this->collectionPermissions]; - } - - public function hasPermission(CollectionPermissions $permission): bool { - return $this->collectionPermissions[$permission->value] ?? false; - } - - public function getLabel(): ?string { - return $this->collectionLabel; - } - - public function setLabel(string $value): self { - $this->collectionLabel = $value; - return $this; - } - - public function getDescription(): ?string { - return $this->collectionDescription; - } - - public function setDescription(?string $value): self { - $this->collectionDescription = $value; - return $this; - } - - public function getPriority(): ?int { - return $this->collectionPriority; - } - - public function setPriority(?int $value): self { - $this->collectionPriority = $value; - return $this; - } - - public function getVisibility(): ?bool { - return $this->collectionVisibility; - } - - public function setVisibility(?bool $value): self { - $this->collectionVisibility = $value; - return $this; - } - - public function getColor(): ?string { - return $this->collectionColor; - } - - public function setColor(?string $value): self { - $this->collectionColor = $value; - return $this; - } - -} diff --git a/lib/Providers/Personal/CollectionProperties.php b/lib/Providers/Personal/CollectionProperties.php new file mode 100644 index 0000000..cf601a1 --- /dev/null +++ b/lib/Providers/Personal/CollectionProperties.php @@ -0,0 +1,67 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalPeople\Providers\Personal; + +use KTXF\People\Collection\CollectionContent; +use KTXF\People\Collection\CollectionPropertiesMutableAbstract; + +/** + * Collection Properties Implementation + */ +class CollectionProperties extends CollectionPropertiesMutableAbstract { + + /** + * Converts store document values into collection properties. + */ + public function fromStore(array $data): static { + $this->data[self::JSON_PROPERTY_CONTENT] = [ + CollectionContent::Individual->value, + CollectionContent::Organization->value, + CollectionContent::Group->value + ]; + + if (isset($data['label'])) { + $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['label']; + } + if (array_key_exists('description', $data)) { + $this->data[self::JSON_PROPERTY_DESCRIPTION] = $data['description']; + } + if (array_key_exists('priority', $data)) { + $this->data[self::JSON_PROPERTY_PRIORITY] = $data['priority'] !== null ? (int) $data['priority'] : null; + } + if (array_key_exists('visibility', $data)) { + $this->data[self::JSON_PROPERTY_VISIBILITY] = $data['visibility'] !== null ? (bool) $data['visibility'] : null; + } + if (array_key_exists('color', $data)) { + $this->data[self::JSON_PROPERTY_COLOR] = $data['color']; + } + if (isset($data['content'])) { + $this->data[self::JSON_PROPERTY_CONTENT] = (string) $data['content']; + } + + return $this; + } + + /** + * Converts collection properties into store document values. + */ + public function toStore(): array { + $content = $this->content(); + + return array_filter([ + 'label' => $this->getLabel(), + 'description' => $this->getDescription(), + 'priority' => $this->getPriority(), + 'visibility' => $this->getVisibility(), + 'color' => $this->getColor(), + 'content' => $content instanceof CollectionContent ? $content->value : null, + ], 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..4e90b56 --- /dev/null +++ b/lib/Providers/Personal/CollectionResource.php @@ -0,0 +1,101 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalPeople\Providers\Personal; + +use KTXF\People\Collection\CollectionMutableAbstract; + +class CollectionResource extends CollectionMutableAbstract { + + private ?string $userId = null; + private ?string $collectionUuid = null; + private bool $collectionEnabled = true; + + 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; + } + + if (isset($data['cid'])) { + $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['cid']; + } elseif (isset($data['_id'])) { + if (is_object($data['_id']) && method_exists($data['_id'], '__toString')) { + $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']; + } elseif (is_array($data['_id']) && isset($data['_id']['$oid'])) { + $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']['$oid']; + } else { + $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']; + } + } + $this->userId = $data['uid'] ?? null; + $this->collectionUuid = isset($data['uuid']) ? (string) $data['uuid'] : null; + $this->getProperties()->fromStore($data); + $this->data[self::JSON_PROPERTY_CREATED] = $data['createdOn'] ?? $data['created'] ?? null; + $this->data[self::JSON_PROPERTY_MODIFIED] = $data['modifiedOn'] ?? $data['modified'] ?? null; + $this->collectionEnabled = $data['enabled'] ?? true; + $this->data[self::JSON_PROPERTY_SIGNATURE] = isset($data['signature']) ? md5((string) $data['signature']) : null; + + return $this; + } + + public function toStore(): array + { + $properties = $this->getProperties(); + + $data = array_filter([ + 'uid' => $this->userId, + 'uuid' => $this->collectionUuid, + 'label' => $properties->getLabel(), + 'description' => $properties->getDescription(), + 'priority' => $properties->getPriority(), + 'visibility' => $properties->getVisibility(), + 'color' => $properties->getColor(), + 'createdOn' => $this->data[self::JSON_PROPERTY_CREATED] ?? null, + 'modifiedOn' => $this->data[self::JSON_PROPERTY_MODIFIED] ?? null, + 'signature' => $this->data[self::JSON_PROPERTY_SIGNATURE] ?? null, + 'enabled' => $this->collectionEnabled, + ], static fn($value) => $value !== null); + + if ($this->identifier() !== null) { + $data['cid'] = (string) $this->identifier(); + } + + return $data; + } + + public function uuid(): ?string { + return $this->collectionUuid; + } + + public function getEnabled(): bool { + return (bool)$this->collectionEnabled; + } + + public function setEnabled(bool $value): self { + $this->collectionEnabled = $value; + return $this; + } + + public function getProperties(): CollectionProperties { + if (!isset($this->properties)) { + $this->properties = new CollectionProperties([]); + } + + return $this->properties; + } + +} diff --git a/lib/Providers/Personal/Entity.php b/lib/Providers/Personal/Entity.php deleted file mode 100644 index a2e9be1..0000000 --- a/lib/Providers/Personal/Entity.php +++ /dev/null @@ -1,162 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\PeopleProviderLocal\Providers\Personal; - -use DateTimeImmutable; -use KTXF\People\Entity\IEntityBase; -use KTXF\People\Entity\IEntityMutable; -use KTXF\People\Entity\Individual\IndividualObject; - -/** - * Entity wrapper - contains metadata and EntityData - */ -class Entity implements IEntityBase, IEntityMutable { - - // Metadata fields (system-managed) - private ?string $entityId = null; - private ?string $tenantId = null; - private ?string $userId = null; - private ?string $collectionId = null; - private ?string $createdOn = null; - private ?string $modifiedOn = null; - private ?string $entitySignature = null; - - // Entity display properties - private ?int $entityPriority = null; - private ?bool $entityVisibility = null; - private ?string $entityColor = null; - private string|array|null $entityData = null; - - public function jsonSerialize(): mixed { - return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_IN => $this->collectionId, - self::JSON_PROPERTY_ID => $this->entityId, - self::JSON_PROPERTY_DATA => $this->entityData, - self::JSON_PROPERTY_SIGNATURE => $this->entitySignature, - ]; - } - - public function jsonDeserialize(array|string $data): static - { - if (is_string($data)) { - $data = json_decode($data, true); - } - - $this->entityId = $data[self::JSON_PROPERTY_ID] ?? null; - $this->collectionId = $data[self::JSON_PROPERTY_IN] ?? null; - $this->entitySignature = $data[self::JSON_PROPERTY_SIGNATURE] ?? null; - $this->entityData = $data[self::JSON_PROPERTY_DATA] ?? null; - - return $this; - } - - public function fromStore(array|object $document): self { - // Convert object to array if needed - if (is_object($document)) { - $document = (array) $document; - } - - // Load metadata - $this->entityId = $document['eid'] ?? null; - $this->tenantId = $document['tid'] ?? null; - $this->userId = $document['uid'] ?? null; - $this->collectionId = $document['cid'] ?? null; - $this->createdOn = $document['createdOn'] ?? null; - $this->modifiedOn = $document['modifiedOn'] ?? null; - $this->entityData = $document['data'] ?? null; - $this->entitySignature = md5(json_encode($this->entityData)); - - return $this; - } - - public function toStore(): array { - $document = [ - 'tid' => $this->tenantId, - 'uid' => $this->userId, - 'cid' => $this->collectionId, - 'eid' => $this->entityId, - 'createdOn' => $this->createdOn ?? date('c'), - 'modifiedOn' => date('c'), - 'data' => $this->entityData, - ]; - - return $document; - } - - public function in(): string|int { - return $this->collectionId ?? ''; - } - - public function id(): string|int { - return $this->entityId ?? ''; - } - - public function created(): ?DateTimeImmutable { - return $this->createdOn ? new DateTimeImmutable($this->createdOn) : null; - } - - public function modified(): ?DateTimeImmutable { - return $this->modifiedOn ? new DateTimeImmutable($this->modifiedOn) : null; - } - - public function signature(): ?string { - return $this->entitySignature; - } - - public function getPriority(): ?int { - return $this->entityPriority; - } - - public function setPriority(?int $value): static { - $this->entityPriority = $value; - return $this; - } - - public function getVisibility(): ?bool { - return $this->entityVisibility; - } - - public function setVisibility(?bool $value): static { - $this->entityVisibility = $value; - return $this; - } - - public function getColor(): ?string { - return $this->entityColor; - } - - public function setColor(?string $value): static { - $this->entityColor = $value; - return $this; - } - - public function getDataObject(): IndividualObject|null { - return $this->entityData ? (new IndividualObject)->jsonDeserialize($this->entityData) : null; - } - - public function setDataObject(IndividualObject $value): static - { - $this->entityData = $value->jsonSerialize(); - return $this; - } - - public function getDataJson(): array|string|null { - return $this->entityData; - } - - public function setDataJson(array|string $value): static - { - $this->entityData = $value; - return $this; - } - -} - diff --git a/lib/Providers/Personal/EntityProperties.php b/lib/Providers/Personal/EntityProperties.php new file mode 100644 index 0000000..ff58ba5 --- /dev/null +++ b/lib/Providers/Personal/EntityProperties.php @@ -0,0 +1,50 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalPeople\Providers\Personal; + +use KTXF\People\Entity\EntityPropertiesMutableAbstract; + +class EntityProperties extends EntityPropertiesMutableAbstract { + + public function jsonSerialize(): array { + $data = $this->getDataRaw(); + + if (is_array($data)) { + return $data; + } + + return []; + } + + public function jsonDeserialize(array|string $data): static { + if (is_string($data)) { + $data = json_decode($data, true); + } + + if (is_array($data) && array_key_exists('data', $data)) { + $this->setDataRaw($data['data']); + return $this; + } + + $this->setDataRaw($data); + return $this; + } + + public function fromStore(array $data): static { + $this->setDataRaw($data['data'] ?? null); + return $this; + } + + public function toStore(): array { + return array_filter([ + 'data' => $this->getDataRaw(), + ], 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..afda1e3 --- /dev/null +++ b/lib/Providers/Personal/EntityResource.php @@ -0,0 +1,93 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderLocalPeople\Providers\Personal; + +use KTXF\People\Entity\EntityMutableAbstract; +use KTXF\People\Entity\Individual\IndividualObject; + +class EntityResource extends EntityMutableAbstract { + + private ?string $tenantId = null; + private ?string $userId = null; + private ?IndividualObject $entityDataObject = null; + + public function __construct( + string $provider = 'default', + string|int $service = 'personal', + ) { + parent::__construct($provider, $service); + } + + public function fromStore(array|object $document): self { + if (is_object($document)) { + $document = (array) $document; + } + + $this->data[self::JSON_PROPERTY_IDENTIFIER] = $document['eid'] ?? null; + $this->tenantId = $document['tid'] ?? null; + $this->userId = $document['uid'] ?? null; + $this->data[self::JSON_PROPERTY_COLLECTION] = $document['cid'] ?? null; + $this->data[self::JSON_PROPERTY_CREATED] = $document['createdOn'] ?? null; + $this->data[self::JSON_PROPERTY_MODIFIED] = $document['modifiedOn'] ?? null; + $this->getProperties()->fromStore($document); + $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($this->getDataJson())); + $this->entityDataObject = null; + + return $this; + } + + public function toStore(): array { + $document = array_filter([ + 'tid' => $this->tenantId, + 'uid' => $this->userId, + 'cid' => $this->collection(), + 'eid' => $this->identifier(), + 'createdOn' => $this->data[self::JSON_PROPERTY_CREATED] ?? date('c'), + 'modifiedOn' => date('c'), + ], static fn($value) => $value !== null); + + $document = array_merge($document, $this->getProperties()->toStore()); + + return $document; + } + + public function getProperties(): EntityProperties { + if (!isset($this->properties)) { + $this->properties = new EntityProperties([]); + } + + return $this->properties; + } + + public function getDataObject(): IndividualObject|null { + return $this->entityDataObject; + } + + public function setDataObject(IndividualObject $value): static + { + $this->entityDataObject = $value; + $this->setDataJson($value->jsonSerialize()); + return $this; + } + + public function getDataJson(): array|string|null { + return $this->getProperties()->getDataRaw(); + } + + public function setDataJson(array|string|null $value): static + { + $this->entityDataObject = null; + $this->getProperties()->setDataRaw($value); + $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($value)); + return $this; + } + +} + diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index 1e95c99..5958cae 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -7,14 +7,16 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\PeopleProviderLocal\Providers\Personal; +namespace KTXM\ProviderLocalPeople\Providers\Personal; -use KTXF\People\Collection\ICollectionMutable; -use KTXF\People\Entity\IEntityBase; -use KTXF\People\Service\IServiceBase; -use KTXF\People\Entity\IEntityMutable; -use KTXF\People\Service\IServiceCollectionMutable; -use KTXF\People\Service\IServiceEntityMutable; +use KTXF\People\Collection\CollectionBaseInterface; +use KTXF\People\Collection\CollectionMutableInterface; +use KTXF\People\Entity\EntityMutableInterface; +use KTXF\People\Service\ServiceBaseInterface; +use KTXF\People\Service\ServiceCollectionMutableInterface; +use KTXF\People\Service\ServiceEntityMutableInterface; +use KTXF\Resource\Delta\Delta; +use KTXF\Resource\Delta\DeltaCollection; use KTXF\Resource\Exceptions\InvalidParameterException; use KTXF\Resource\Filter\Filter; use KTXF\Resource\Filter\IFilter; @@ -23,84 +25,84 @@ use KTXF\Resource\Range\RangeTally; use KTXF\Resource\Range\RangeType; use KTXF\Resource\Sort\ISort; use KTXF\Resource\Sort\Sort; -use KTXM\PeopleProviderLocal\Store\Personal\Store; +use KTXM\ProviderLocalPeople\Store\Personal\Store; -class PersonalService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable { +class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { - protected const SERVICE_ID = 'personal'; - protected const SERVICE_LABEL = 'Personal Contacts Service'; - protected const SERVICE_PROVIDER = 'default'; + public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; + + private const PROVIDER_IDENTIFIER = 'default'; + private const SERVICE_IDENTIFIER = 'personal'; + private const SERVICE_LABEL = 'Personal Calendar Service'; protected array $serviceCollectionCache = []; protected ?string $serviceTenantId = null; protected ?string $serviceUserId = null; protected ?bool $serviceEnabled = true; - protected array $serviceAbilities = [ + private array $serviceAbilities = [ self::CAPABILITY_COLLECTION_LIST => true, self::CAPABILITY_COLLECTION_LIST_FILTER => [ - 'id' => 'a:10:64:192', - 'label' => 's:100:256:771', - 'description' => 's:100:256:771', + self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:100:256:256', ], self::CAPABILITY_COLLECTION_LIST_SORT => [ - 'label', - 'description' + self::CAPABILITY_COLLECTION_SORT_LABEL, + self::CAPABILITY_COLLECTION_SORT_RANK, ], 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_CREATE => true, + self::CAPABILITY_COLLECTION_UPDATE => true, + self::CAPABILITY_COLLECTION_DELETE => true, self::CAPABILITY_ENTITY_LIST => true, self::CAPABILITY_ENTITY_LIST_FILTER => [ - '*' => 's:200:256:771', - 'uri' => 's:200:256:771', - 'label' => 's:200:256:771', - 'phone' => 's:200:256:771', - 'email' => 's:200:256:771', - 'location' => 's:200: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', - 'phone', - 'email', - 'location' + self::CAPABILITY_ENTITY_SORT_ID, + self::CAPABILITY_ENTITY_SORT_URID, ], 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_DELTA => true, self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => 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, ]; public function __construct( private Store $store, ) {} - 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): self { + public function initialize(string $tenantId, string $userId): self { $this->serviceTenantId = $tenantId; $this->serviceUserId = $userId; 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])) { return (bool)$this->serviceAbilities[$value]; @@ -112,12 +114,13 @@ 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 { @@ -128,7 +131,30 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return (bool)$this->serviceEnabled; } - public function collectionList(?IFilter $filter = null, ?ISort $sort = null): array { + 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 + + public function collectionList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null): array { $entries = $this->store->collectionList($this->serviceTenantId, $this->serviceUserId, $filter, $sort); $this->serviceCollectionCache = $entries; return $entries ?? []; @@ -142,49 +168,70 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } - public function collectionExtant(string|int $id): bool { - // determine if collection is cached - if (isset($this->serviceCollectionCache[$id])) { - return true; + public function collectionExtant(string|int $location, string|int ...$identifiers): array { + $resolvedIdentifiers = $identifiers !== [] ? $identifiers : [$location]; + $response = []; + + foreach ($resolvedIdentifiers as $identifier) { + if (isset($this->serviceCollectionCache[$identifier])) { + $response[$identifier] = true; + continue; + } + + $exists = $this->store->collectionExtant($this->serviceTenantId, $this->serviceUserId, (string)$identifier); + $response[$identifier] = $exists; + + if ($exists) { + $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); + if ($collection !== null) { + $this->serviceCollectionCache[$identifier] = $collection; + } + } } - // retrieve from store - return $this->store->collectionExtant($this->serviceTenantId, $this->serviceUserId, $id); + + return $response; } - public function collectionFetch(string|int $id): ?Collection { + public function collectionFetch(string|int $identifier): ?CollectionBaseInterface { // determine if collection is cached - if (isset($this->serviceCollectionCache[$id])) { - return $this->serviceCollectionCache[$id]; + if (isset($this->serviceCollectionCache[$identifier])) { + return $this->serviceCollectionCache[$identifier]; } // retrieve from store - $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, $id); + $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); if ($collection !== null) { - $this->serviceCollectionCache[$collection->id()] = $collection; + $collectionIdentifier = $collection->identifier(); + if ($collectionIdentifier !== null) { + $this->serviceCollectionCache[(string) $collectionIdentifier] = $collection; + } return $collection; } return null; } - public function collectionFresh(): Collection { - return new Collection(); + public function collectionFresh(): CollectionResource { + return new CollectionResource(); } - public function collectionCreate(string|int $location, ICollectionMutable $collection, array $options): Collection { + public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface { // convert collection to a native type if needed - if (!($collection instanceof Collection)) { - $nativeCollection = new Collection(); + if (!($collection instanceof CollectionResource)) { + $nativeCollection = new CollectionResource(); $nativeCollection->jsonDeserialize($collection->jsonSerialize()); } else { $nativeCollection = clone $collection; } // create collection in store $result = $this->store->collectionCreate($this->serviceTenantId, $this->serviceUserId, $nativeCollection); - $this->serviceCollectionCache[$result->id()] = $result; + $resultIdentifier = $result->identifier(); + if ($resultIdentifier !== null) { + $this->serviceCollectionCache[(string) $resultIdentifier] = $result; + } return $result; } - public function collectionModify(string|int $id, ICollectionMutable $collection): Collection { + public function collectionUpdate(string|int $id, CollectionMutableInterface $collection): CollectionBaseInterface { // validate id if (!is_string($id)) { throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); @@ -194,18 +241,25 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'"); } // convert collection to a native type if needed - if (!($collection instanceof Collection)) { - $nativeCollection = new Collection(); - $nativeCollection->jsonDeserialize($collection->jsonSerialize()); + if (!($collection instanceof CollectionResource)) { + $nativeCollection = new CollectionResource(); + $data = $collection->jsonSerialize(); + $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; + $nativeCollection->jsonDeserialize($data); } else { $nativeCollection = clone $collection; + if ($nativeCollection->identifier() === null) { + $data = $nativeCollection->jsonSerialize(); + $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; + $nativeCollection->jsonDeserialize($data); + } } // modify collection in store $result = $this->store->collectionModify($this->serviceTenantId, $this->serviceUserId, $nativeCollection); return $result; } - public function collectionDestroy(string|int $id): bool { + public function collectionDelete(string|int $id, bool $force = false, bool $recursive = false): bool { // validate id if (!is_string($id)) { throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); @@ -222,6 +276,8 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return false; } + // Entity operations + public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $options = null): array { // validate id if (!is_string($collection)) { @@ -236,7 +292,6 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $entries ?? []; } - public function entityListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } @@ -247,10 +302,13 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi public function entityListRange(RangeType $type): IRange { // validate type - if ($type !== RangeType::TALLY) { - throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid"); + if ($type === RangeType::TALLY) { + return new RangeTally(); } - return new RangeTally(); + if ($type === RangeType::DATE) { + return new RangeDate(); + } + throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid"); } public function entityExtant(string|int $collection, string|int ...$identifiers): array { @@ -266,7 +324,7 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $this->store->entityExtant($collection, ...$identifiers); } - public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): array { + public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta { // validate id if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); @@ -276,7 +334,14 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entity delta from store - return $this->store->chronicleReminisce($this->serviceTenantId, $this->serviceUserId,$collection, $signature); + $delta = $this->store->chronicleReminisce($this->serviceTenantId, $collection, $signature); + + return new Delta( + new DeltaCollection($delta['additions'] ?? []), + new DeltaCollection($delta['modifications'] ?? []), + new DeltaCollection($delta['deletions'] ?? []), + $delta['signature'] ?? '' + ); } public function entityFetch(string|int $collection, string|int ...$identifiers): array { @@ -293,11 +358,11 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $entries ?? []; } - public function entityFresh(): Entity { - return new Entity(); + public function entityFresh(): EntityResource { + return new EntityResource(); } - public function entityCreate(string|int $collection, IEntityMutable $entity, array $options): Entity { + public function entityCreate(string|int $collection, EntityMutableInterface $entity, array $options = []): EntityResource { // validate collection identifier if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); @@ -307,7 +372,7 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // convert entity to a native type if needed - if (!($entity instanceof Entity)) { + if (!($entity instanceof EntityResource)) { $nativeEntity = $this->entityFresh(); $nativeEntity->jsonDeserialize($entity->jsonSerialize()); } else { @@ -319,7 +384,7 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $result; } - public function entityModify(string|int $collection, string|int $identifier, IEntityMutable $entity): Entity { + public function entityUpdate(string|int $collection, string|int $identifier, EntityMutableInterface $entity): EntityResource { // validate collection identifier if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); @@ -352,7 +417,7 @@ class PersonalService implements IServiceBase, IServiceCollectionMutable, IServi return $result; } - public function entityDestroy(string|int $collection, string|int $identifier): IEntityBase { + public function entityDelete(string|int $collection, string|int $identifier): EntityMutableInterface { // validate collection identifier if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index b4a577f..09ee577 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -7,18 +7,17 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\PeopleProviderLocal\Providers; +namespace KTXM\ProviderLocalPeople\Providers; use Psr\Container\ContainerInterface; -use KTXF\People\Provider\IProviderBase; -use KTXF\People\Service\IServiceBase; -use KTXF\Resource\Provider\ProviderInterface; -use KTXM\PeopleProviderLocal\Providers\Personal\PersonalService; -use KTXM\PeopleProviderLocal\Providers\Shared\SharedService; +use KTXF\People\Provider\ProviderBaseInterface; +use KTXF\People\Service\ServiceBaseInterface; +use KTXM\ProviderLocalPeople\Providers\Personal\PersonalService; -class Provider implements IProviderBase, ProviderInterface { +class Provider implements ProviderBaseInterface { - protected const PROVIDER_ID = 'default'; + public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; + protected const PROVIDER_IDENTIFIER = 'default'; protected const PROVIDER_LABEL = 'Default People Provider'; protected const PROVIDER_DESCRIPTION = 'Provides local people storage'; protected const PROVIDER_ICON = 'user'; @@ -34,21 +33,21 @@ class Provider implements IProviderBase, ProviderInterface { private readonly ContainerInterface $container, ) {} - public function jsonSerialize(): mixed - { - return $this->toJson(); - } - - public function toJson(): array + public function jsonSerialize(): 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, - ]; + 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, + ]; } + public function jsonDeserialize(array|string $data): static + { + return $this; + } + public function type(): string { return self::TYPE_PEOPLE; @@ -56,7 +55,7 @@ class Provider implements IProviderBase, ProviderInterface { public function identifier(): string { - return self::PROVIDER_ID; + return self::PROVIDER_IDENTIFIER; } public function label(): string { @@ -83,83 +82,28 @@ class Provider implements IProviderBase, ProviderInterface { return $this->providerAbilities; } - public function id(): string - { - return self::PROVIDER_ID; + + protected function serviceInstancePersonal(string $tenantId, string $userId): PersonalService { + $service = $this->container->get(PersonalService::class); + $service->initialize($tenantId, $userId); + return $service; } public function serviceList(string $tenantId, string $userId, array $filter = []): array { // if no filter is provided, return all services if ($filter === []) { - $filter = ['personal', 'shared']; + $filter = ['personal']; } // 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); - return $service; - } - - /** - * construct service object instance - * - * @since 1.0.0 - * - * @return SharedService blank service instance - */ - protected function serviceInstanceShared(string $tenantId, string $userId): SharedService { - $service = $this->container->get(SharedService::class); - $service->init($tenantId, $userId); - return $service; - } - - /** - * Determine if any services are configured for a specific user - * - * @since 1.0.0 - * - * @inheritdoc - */ - 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 { + public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?ServiceBaseInterface { // check if services are cached if (isset($this->servicesCache[$userId][$identifier])) { @@ -169,14 +113,20 @@ class Provider implements IProviderBase, ProviderInterface { 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]; } + public function serviceExtant(string $tenantId, string $userId, int|string ...$identifiers): array { + $data = []; + foreach ($identifiers as $id) { + $data[$id] = match ($id) { + 'personal' => true, + default => false, + }; + } + return $data; + } + } diff --git a/lib/Providers/Shared/Collection.php b/lib/Providers/Shared/Collection.php deleted file mode 100644 index 76ad78b..0000000 --- a/lib/Providers/Shared/Collection.php +++ /dev/null @@ -1,342 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\PeopleProviderLocal\Providers\Shared; - -use KTXF\People\Collection\CollectionContent; -use KTXF\People\Collection\CollectionPermissions; -use KTXF\People\Collection\CollectionRoles; -use KTXF\People\Collection\ICollectionMutable; -use KTXM\PeopleProviderLocal\Store\Personal\CollectionEntry; -use Override; - -class Collection implements ICollectionMutable { - - private ?string $userId = null; - private ?int $collectionShareId = null; - private ?string $collectionShareOwner = null; - private ?int $collectionSharePermissions = null; - private string $providerId = 'default'; - private string $serviceId = 'shared'; - private ?int $collectionId = null; - private ?string $collectionUuid = null; - private ?string $collectionLabel = null; - private ?string $collectionDescription = null; - private ?int $collectionPriority = null; - private ?bool $collectionVisibility = null; - private ?string $collectionColor = null; - private bool $collectionEnabled = true; - private ?string $collectionSignature = null; - private array $collectionPermissions = []; - private array $collectionAttributes = [ - 'roles' => [ - CollectionRoles::Individual->value => true, - ], - 'contents' => [ - CollectionContent::Individual->value => true, - CollectionContent::Organization->value => true, - CollectionContent::Group->value => true, - ], - ]; - - public function jsonSerialize(): mixed { - return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_PROVIDER => $this->providerId, - self::JSON_PROPERTY_SERVICE => $this->serviceId, - self::JSON_PROPERTY_IN => null, - self::JSON_PROPERTY_ID => $this->collectionId, - self::JSON_PROPERTY_UUID => $this->collectionUuid, - self::JSON_PROPERTY_LABEL => $this->collectionLabel, - self::JSON_PROPERTY_DESCRIPTION => $this->collectionDescription, - self::JSON_PROPERTY_PRIORITY => $this->collectionPriority, - self::JSON_PROPERTY_VISIBILITY => $this->collectionVisibility, - self::JSON_PROPERTY_COLOR => $this->collectionColor, - self::JSON_PROPERTY_ENABLED => $this->collectionEnabled, - self::JSON_PROPERTY_SIGNATURE => $this->collectionSignature, - self::JSON_PROPERTY_PERMISSIONS => [$this->userId => $this->collectionPermissions], - self::JSON_PROPERTY_ROLES => $this->collectionAttributes['roles'] ?? [], - self::JSON_PROPERTY_CONTENTS => $this->collectionAttributes['contents'] ?? [], - ]; - } - - public function jsonDeserialize(array $data): static - { - $this->collectionId = $data[self::JSON_PROPERTY_ID] ?? null; - $this->collectionUuid = $data[self::JSON_PROPERTY_UUID] ?? null; - $this->collectionLabel = $data[self::JSON_PROPERTY_LABEL] ?? null; - $this->collectionDescription = $data[self::JSON_PROPERTY_DESCRIPTION] ?? null; - $this->collectionPriority = $data[self::JSON_PROPERTY_PRIORITY] ?? null; - $this->collectionVisibility = $data[self::JSON_PROPERTY_VISIBILITY] ?? null; - $this->collectionColor = $data[self::JSON_PROPERTY_COLOR] ?? null; - $this->collectionEnabled = $data[self::JSON_PROPERTY_ENABLED] ?? true; - $this->collectionSignature = $data[self::JSON_PROPERTY_SIGNATURE] ?? null; - - return $this; - } - - public function fromStore(CollectionEntry $data): self { - $this->collectionShareOwner = $data->getUserId(); - $this->collectionId = $data->getId(); - $this->collectionUuid = $data->getUri(); - $this->collectionLabel = $data->getDisplayname(); - $this->collectionDescription = $data->getDescription(); - $this->collectionSignature = $data->getSynctoken(); - return $this; - } - - public function toStore(): CollectionEntry { - $data = new CollectionEntry(); - if ($this->collectionId !== null) { - $data->setId($this->collectionId); - } - $data->setUserId($this->userId); - $data->setUri($this->collectionUuid); - $data->setDisplayname($this->collectionLabel); - $data->setDescription($this->collectionDescription); - return $data; - } - - public function fromShareStore(array $data): self { - - if (empty($data['principaluri']) || !str_starts_with($data['principaluri'], 'principals/users/')) { - throw new \InvalidArgumentException('Share data must contain a principaluri'); - } - $this->userId = substr($data['principaluri'], 17); - $this->collectionShareId = $data['id'] ?? null; - $this->collectionSharePermissions = $data['access'] ?? 0; - $this->collectionPermissions[CollectionPermissions::View->value] = true; - if ($this->collectionSharePermissions === 2) { - $this->collectionPermissions[CollectionPermissions::Create->value] = true; - $this->collectionPermissions[CollectionPermissions::Modify->value] = true; - $this->collectionPermissions[CollectionPermissions::Destroy->value] = true; - } - return $this; - } - - /** - * Unique identifier of the service this collection belongs to - * - * @since 2025.05.01 - */ - public function in(): null { - return null; - } - - /** - * Unique arbitrary text string identifying this service (e.g. 1 or collection1 or anything else) - * - * @since 2025.05.01 - */ - public function id(): int { - return $this->collectionShareId; - } - - /** - * Lists all supported attributes - * - * @since 2025.05.01 - * - * @return array - */ - public function attributes(): array { - return $this->collectionAttributes; - } - - /** - * Unique universal identifier - * - * @since 2025.05.01 - */ - public function uuid(): string { - return $this->collectionUuid; - } - - /** - * Gets the signature of this collection - * - * @since 2025.05.01 - */ - public function signature(): ?string { - return $this->collectionSignature; - } - - /** - * Gets the roles of this collection - * - * @since 2025.05.01 - * - * @return array - */ - public function roles(): array { - return $this->collectionAttributes['roles'] ?? []; - } - - /** - * Checks if this collection supports the given role - * - * @since 2025.05.01 - */ - public function role(CollectionRoles $role): bool { - return $this->collectionAttributes['roles'][$role->value] ?? false; - } - - /** - * Gets the content types of this collection - * - * @since 2025.05.01 - * - * @return array - */ - public function contents(): array { - return $this->collectionAttributes['content'] ?? []; - } - - /** - * Checks if this collection contains the given content type - * - * @since 2025.05.01 - */ - public function contains(CollectionContent $content): bool { - return $this->collectionAttributes['content'][$content->value] ?? false; - } - - /** - * Gets the active status of this collection - * - * @since 2025.05.01 - */ - public function getEnabled(): bool { - return (bool)$this->collectionEnabled; - } - - /** - * Sets the active status of this collection - * - * @since 2025.05.01 - */ - public function setEnabled(bool $value): self { - $this->collectionEnabled = $value; - return $this; - } - - /** - * Gets the active status of this collection - * - * @since 2025.05.01 - */ - public function getPermissions(): array { - return [$this->userId => $this->collectionPermissions]; - } - - /** - * Checks if this collection supports the given attribute - * - * @since 2025.05.01 - */ - public function hasPermission(CollectionPermissions $permission): bool { - return $this->collectionPermissions[$permission->value] ?? false; - } - - /** - * Gets the human friendly name of this collection (e.g. Personal Contacts) - * - * @since 2025.05.01 - */ - public function getLabel(): ?string { - return $this->collectionLabel; - } - - /** - * Sets the human friendly name of this collection (e.g. Personal Contacts) - * - * @since 2025.05.01 - */ - public function setLabel(string $value): self { - $this->collectionLabel = $value; - return $this; - } - - /** - * Gets the human friendly description of this collection - * - * @since 2025.05.01 - */ - public function getDescription(): ?string { - return $this->collectionDescription; - } - - /** - * Sets the human friendly description of this collection - * - * @since 2025.05.01 - */ - public function setDescription(?string $value): self { - $this->collectionDescription = $value; - return $this; - } - - /** - * Gets the priority of this collection - * - * @since 2025.05.01 - */ - public function getPriority(): ?int { - return $this->collectionPriority; - } - - /** - * Sets the priority of this collection - * - * @since 2025.05.01 - */ - public function setPriority(?int $value): self { - $this->collectionPriority = $value; - return $this; - } - - /** - * Gets the visibility of this collection - * - * @since 2025.05.01 - */ - public function getVisibility(): ?bool { - return $this->collectionVisibility; - } - - /** - * Sets the visibility of this collection - * - * @since 2025.05.01 - */ - public function setVisibility(?bool $value): self { - $this->collectionVisibility = $value; - return $this; - } - - /** - * Gets the color of this collection - * - * @since 2025.05.01 - */ - public function getColor(): ?string { - return $this->collectionColor; - } - - /** - * Sets the color of this collection - * - * @since 2025.05.01 - */ - public function setColor(?string $value): self { - $this->collectionColor = $value; - return $this; - } - -} diff --git a/lib/Providers/Shared/SharedService.php b/lib/Providers/Shared/SharedService.php deleted file mode 100644 index e3a4276..0000000 --- a/lib/Providers/Shared/SharedService.php +++ /dev/null @@ -1,583 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXM\PeopleProviderLocal\Providers\Shared; - -use KTXF\People\Collection\ICollectionMutable; -use KTXF\People\Entity\IEntityMutable; -use KTXF\People\Exceptions\InvalidParameterException; -use KTXF\People\Exceptions\UnauthorizedException; -use KTXF\People\Exceptions\UnsupportedException; -use KTXF\People\Filter\Filter; -use KTXF\People\Filter\IFilter; -use KTXF\People\Range\IRange; -use KTXF\People\Range\RangeTally; -use KTXF\People\Range\RangeType; -use KTXF\People\Service\IServiceBase; -use KTXF\People\Service\IServiceCollectionMutable; -use KTXF\People\Service\IServiceEntityMutable; -use KTXF\People\Sort\ISort; -use KTXF\People\Sort\Sort; -use KTXM\PeopleProviderLocal\Providers\Personal\Entity; - -class SharedService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable { - - protected const SERVICE_ID = 'shared'; - protected const SERVICE_LABEL = 'Shared Contacts Service'; - protected const SERVICE_PROVIDER = 'default'; - - protected array $serviceCollectionCache = []; - protected array $serviceSharesCache = []; - protected ?string $serviceTenantId = null; - protected ?string $serviceUserId = null; - protected ?bool $serviceEnabled = true; - - protected array $serviceAbilities = [ - self::CAPABILITY_COLLECTION_LIST => true, - self::CAPABILITY_COLLECTION_LIST_FILTER => [ - 'id' => 'a:10:64:192', - 'label' => 's:100:256:771', - 'description' => 's:100:256:771', - ], - self::CAPABILITY_COLLECTION_LIST_SORT => [ - 'label', - 'description' - ], - 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_ENTITY_LIST => true, - self::CAPABILITY_ENTITY_LIST_FILTER => [ - '*' => 's:200:256:771', - 'uri' => 's:200:256:771', - 'label' => 's:200:256:771', - 'phone' => 's:200:256:771', - 'email' => 's:200:256:771', - 'location' => 's:200:256:771' - ], - self::CAPABILITY_ENTITY_LIST_SORT => [ - 'label', - 'phone', - 'email', - 'location' - ], - self::CAPABILITY_ENTITY_LIST_RANGE => [ - 'tally' => ['absolute', 'relative'] - ], - self::CAPABILITY_ENTITY_DELTA => true, - self::CAPABILITY_ENTITY_EXTANT => true, - self::CAPABILITY_ENTITY_FETCH => 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, - ]; - - public function __construct( - private Store $store, - //private SharingMapper $sharingStore, - ) {} - - 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): self { - $this->serviceTenantId = $tenantId; - $this->serviceUserId = $userId; - return $this; - } - - /** - * Confirms if specific capability is supported - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function capable(string $value): bool { - if (isset($this->serviceAbilities[$value])) { - return (bool)$this->serviceAbilities[$value]; - } - return false; - } - - /** - * Lists all supported capabilities - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function capabilities(): array { - return $this->serviceAbilities; - } - - /** - * Unique identifier of the provider this service belongs to - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function in(): string{ - return self::SERVICE_PROVIDER; - } - - /** - * Unique arbitrary text string identifying this service (e.g. 1 or service1 or anything else) - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function id(): string { - return self::SERVICE_ID; - } - - /** - * Gets the localized human friendly name of this service (e.g. ACME Company Mail Service) - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function getLabel(): string { - return self::SERVICE_LABEL; - } - - /** - * Gets the active status of this service - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function getEnabled(): bool { - return (bool)$this->serviceEnabled; - } - - /** - * List of accessible collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionList(?IFilter $filter = null, ?ISort $sort = null): array { - /* - $shareEntries = $this->listShareEntries(); - foreach ($shareEntries as $key => $shareEntry) { - $collectionEntry = $this->store->collectionFetch('system', $shareEntry['resourceid']); - $collection = new Collection(); - $collection->fromStore($collectionEntry)->fromShareStore($shareEntry); - $list[$collection->id()] = $collection; - $this->serviceCollectioncache[$collection->id()] = $collection; - } - */ - return $list ?? []; - } - - /** - * Returns a filter for collection list - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionListFilter(): Filter { - return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); - } - - /** - * Returns a sort for collection list - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionListSort(): Sort { - return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); - } - - /** - * Confirms if a collection exists - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionExtant(string|int $id): bool { - // validate id - if (!is_numeric($id)) { - throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); - } - $id = (int)$id; - // determine if collection is cached - if (isset($this->serviceCollectioncache[$id])) { - return true; - } - if ($this->fetchShareEntry($id) !== null) { - return true; - } - return false; - } - - /** - * Fetches details about a specific collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionFetch(string|int $id): ?Collection { - // validate access - if ($this->collectionExtant($id) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); - } - $id = (int)$id; - // determine if collection is cached - if (isset($this->serviceCollectioncache[$id])) { - return $this->serviceCollectioncache[$id]; - } - // retrieve share data - $shareEntry = $this->fetchShareEntry($id); - // retrieve collection data - $collectionEntry = $this->store->collectionFetch('system', $shareEntry['resourceid']); - if ($collectionEntry !== null) { - $collection = new Collection(); - $collection->fromStore($collectionEntry)->fromShareStore($shareEntry); - $this->serviceCollectioncache[$collection->id()] = $collection; - return $collection; - } - return null; - } - - /** - * Creates a new collection at the specified location - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionCreate(string|int $location, ICollectionMutable $collection, array $options): Collection { - throw new UnsupportedException("Unsupported: Shared service does not support collection creation"); - } - - /** - * Modifies an existing collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionModify(string|int $id, ICollectionMutable $collection): Collection { - // validate access - if ($this->collectionExtant($id) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); - } - $id = (int)$id; - // convert collection to a native type if needed - if (!($collection instanceof Collection)) { - $nativeCollection = new Collection(); - $nativeCollection->fromJson($collection->toJson()); - } else { - $nativeCollection = clone $collection; - } - // convert to store type and force user id - $storeEntry = $nativeCollection->toStore(); - $storeEntry->setUserId($this->serviceUserId); - // modify collection in store - $storeEntry = $this->store->collectionModify($storeEntry); - $nativeCollection->fromStore($storeEntry); - return $nativeCollection; - } - - /** - * Destroys a collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function collectionDestroy(string|int $id): bool { - // validate access - if ($this->collectionExtant($id) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); - } - $id = (int)$id; - // destroy collection in store - if ($this->store->collectionDestroyById($this->serviceUserId, $id)) { - unset($this->serviceCollectioncache[$id]); - return true; - } - return false; - } - - /** - * Lists all entities in a specific collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $options = null): array { - // validate collection access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // retrieve entity entries - $entries = $this->store->entityList($shareEntry['resourceid'], $filter, $sort, $range, $options); - foreach ($entries as $key => $entry) { - $entity = new Entity(); - $entity->fromStore($entry); - $entities[$key] = $entity; - } - return $entities ?? []; - } - - /** - * Returns a filter for entity list - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityListFilter(): Filter { - return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); - } - - /** - * Returns a sort for entity list - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityListSort(): Sort { - return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []); - } - - /** - * Returns a sort for entity list - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityListRange(RangeType $type): IRange { - // validate type - if ($type !== RangeType::TALLY) { - throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid"); - } - return new RangeTally(); - } - - /** - * Confirms if entity(ies) exist in a collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityExtant(string|int $collection, string|int ...$identifiers): array { - // validate access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // retrieve entity status - return $this->store->entityExtant($shareEntry['resourceid'], $identifiers); - } - - /** - * Lists of all changes from a specific token - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): array { - // validate access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // retrieve entity delta from store - return $this->store->chronicleReminisce($shareEntry['resourceid'], $signature); - } - - /** - * Retrieves details about a specific entity(ies) - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityFetch(string|int $collection, string|int ...$identifiers): array { - // validate collection access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // retrieve entity entry - $entries = $this->store->entityFetch($shareEntry['resourceid'], $identifiers); - foreach ($entries as $key => $entry) { - $entity = new Entity(); - $entity->fromStore($entry); - $entities[$key] = $entity; - } - return $entities ?? []; - } - - /** - * Creates a fresh entity of the specified type - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityFresh(): Entity { - return new Entity(); - } - - /** - * Creates a new entity in the specified collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityCreate(string|int $collection, IEntityMutable $entity, array $options): Entity { - // validate collection access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // convert enity to a native type if needed - if (!($entity instanceof Entity)) { - $nativeEntity = $this->entityFresh(); - $nativeEntity->fromJson($entity->toJson()); - } else { - $nativeEntity = clone $entity; - } - // convert to store type and force address book id - $storeEntry = $nativeEntity->toStore(); - $storeEntry->setAddressbookid($shareEntry['resourceid']); - $storeEntry->setLastmodified(time()); - if (isset($options['source']) && $options['source'] === 'dav' && isset($options['uri']) && !empty($options['uri'])) { - $storeEntry->setUri($options['uri']); - } - // create entry in store - $storeEntry = $this->store->entityCreate($storeEntry); - $nativeEntity->fromStore($storeEntry); - - return $nativeEntity; - } - - /** - * Modifies an existing entity in the specified collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityModify(string|int $collection, string|int $identifier, IEntityMutable $entity): Entity { - // validate collection access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // validate entity identifier - if (empty($identifier) || !is_numeric($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } - $identifier = (int)$identifier; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // validate entity extant and ownership - $extant = $this->store->entityExtant($collection, [$identifier]); - if (!isset($extant[$identifier]) || $extant[$identifier] === false) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); - } - // convert enity to a native type if needed - if (!($entity instanceof Entity)) { - $nativeEntity = $this->entityFresh(); - $nativeEntity->fromJson($entity->toJson()); - } else { - $nativeEntity = clone $entity; - } - - // convert to store type and force address book id - $storeEntry = $nativeEntity->toStore(); - $storeEntry->setId($identifier); - $storeEntry->setAddressbookid($shareEntry['resourceid']); - $storeEntry->setLastmodified(time()); - // modify entry in store - $storeEntry = $this->store->entityModify($shareEntry['resourceid'], $storeEntry); - $nativeEntity->fromStore($storeEntry); - - return $nativeEntity; - } - - /** - * Destroys an existing entity in the specified collection - * - * @since 1.0.0 - * - * @inheritDoc - */ - public function entityDestroy(string|int $collection, string|int $identifier): IEntityMutable { - // validate collection access - if ($this->collectionExtant($collection) === false) { - throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); - } - $collection = (int)$collection; - // retrieve share entry - $shareEntry = $this->fetchShareEntry($collection); - // validate entity identifier - if (empty($identifier) || !is_numeric($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } - $identifier = (int)$identifier; - // validate entity extant and ownership - $extant = $this->store->entityExtant($collection, [$identifier]); - if (!isset($extant[$identifier]) || $extant[$identifier] === false) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); - } - // destroy entry in store - $storeEntry = $this->store->entityDestroyById($shareEntry['resourceid'], $identifier); - $nativeEntity = $this->entityFresh(); - $nativeEntity->fromStore($storeEntry); - - return $nativeEntity; - } - -} diff --git a/lib/Store/Personal/Store.php b/lib/Store/Personal/Store.php index 7cdc684..695a917 100644 --- a/lib/Store/Personal/Store.php +++ b/lib/Store/Personal/Store.php @@ -7,7 +7,7 @@ declare(strict_types=1); * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace KTXM\PeopleProviderLocal\Store\Personal; +namespace KTXM\ProviderLocalPeople\Store\Personal; use KTXC\Db\DataStore; use KTXF\Resource\Filter\Filter; @@ -17,15 +17,13 @@ use KTXF\Resource\Range\Range; use KTXF\Resource\Range\RangeType; use KTXF\Resource\Sort\Sort; use KTXF\Utile\UUID; -use KTXM\PeopleProviderLocal\Providers\Personal\Collection; -use KTXM\PeopleProviderLocal\Providers\Personal\Entity; +use KTXM\ProviderLocalPeople\Providers\Personal\CollectionResource; +use KTXM\ProviderLocalPeople\Providers\Personal\EntityResource; class Store { protected string $_CollectionTable = 'people_provider_local_collection'; - protected string $_CollectionClass = 'KTXM\PeopleProviderLocal\Providers\Personal\Collection'; protected string $_EntityTable = 'people_provider_local_entity'; - protected string $_EntityClass = 'KTXM\PeopleProviderLocal\Providers\Personal\Entity'; protected string $_ChronicleTable = 'people_provider_local_chronicle'; protected array $_CollectionFilterAttributeMap = [ @@ -145,8 +143,11 @@ class Store { $cursor = $this->_store->selectCollection($this->_CollectionTable)->find($query, $options); $list = []; foreach ($cursor as $entry) { - $entry = (new Collection())->fromStore($entry); - $list[$entry->id()] = $entry; + $entry = $this->collectionFresh()->fromStore($entry); + $identifier = $entry->identifier(); + if ($identifier !== null) { + $list[(string) $identifier] = $entry; + } } return $list; } @@ -177,9 +178,9 @@ class Store { * @param string $userId user identifier * @param string $identifier collection identifier * - * @return Collection + * @return CollectionResource */ - public function collectionFetch(string $tenantId, string $userId, string $identifier): ?Collection { + public function collectionFetch(string $tenantId, string $userId, string $identifier): ?CollectionResource { $cursor = $this->_store->selectCollection($this->_CollectionTable)->findOne([ 'tid' => $tenantId, 'uid' => $userId, @@ -188,7 +189,7 @@ class Store { if ($cursor === null) { return null; } - $entry = (new Collection())->fromStore($cursor); + $entry = $this->collectionFresh()->fromStore($cursor); return $entry; } @@ -197,10 +198,10 @@ class Store { * * @since Release 1.0.0 * - * @return Collection + * @return CollectionResource */ - public function collectionFresh(): Collection { - return new $this->_CollectionClass; + public function collectionFresh(): CollectionResource { + return new CollectionResource(); } /** @@ -209,24 +210,24 @@ class Store { * @since Release 1.0.0 * * @param string $userId user identifier - * @param Collection $entity + * @param CollectionResource $entity * - * @return Collection + * @return CollectionResource */ - public function collectionCreate(string $tenantId, string $userId, Collection $entity): Collection { + public function collectionCreate(string $tenantId, string $userId, CollectionResource $entity): CollectionResource { // convert entity to store format $data = $entity->toStore(); // prepare data for creation $data['tid'] = $tenantId; $data['uid'] = $userId; $data['cid'] = UUID::v4(); + $data['signature'] = isset($data['signature']) && is_numeric($data['signature']) ? (int)$data['signature'] : 0; $data['createdOn'] = date('c'); $data['modifiedOn'] = $data['createdOn']; // create entry $result = $this->_store->selectCollection($this->_CollectionTable)->insertOne($data); if ($result->getInsertedCount() === 1) { - $entity = new Collection(); - $entity->fromStore($data); + $entity = $this->collectionFresh()->fromStore($data); } return $entity; } @@ -237,15 +238,18 @@ class Store { * @since Release 1.0.0 * * @param string $userId user identifier - * @param Collection $entity + * @param CollectionResource $entity * - * @return Collection + * @return CollectionResource */ - public function collectionModify(string $tenantId, string $userId, Collection $entity): Collection { + public function collectionModify(string $tenantId, string $userId, CollectionResource $entity): CollectionResource { // convert entity to store format $data = $entity->toStore(); // prepare data for modification - $cid = $entity->id(); + $cid = $entity->identifier(); + if ($cid === null) { + throw new \InvalidArgumentException('Collection identifier is required for modification'); + } $data['modifiedOn'] = date('c'); unset($data['_id'], $data['tid'], $data['uid'], $data['cid']); // modify entry @@ -261,12 +265,16 @@ class Store { * * @since Release 1.0.0 * - * @param Collection $entity + * @param CollectionResource $entity * - * @return Collection + * @return CollectionResource */ - public function collectionDestroy(string $tenantId, string $userId, Collection $entity): Collection { - return $this->collectionDestroyById($tenantId, $userId, $entity->id()) ? $entity : $entity; + public function collectionDestroy(string $tenantId, string $userId, CollectionResource $entity): CollectionResource { + $identifier = $entity->identifier(); + if ($identifier === null) { + return $entity; + } + return $this->collectionDestroyById($tenantId, $userId, (string) $identifier) ? $entity : $entity; } /** @@ -319,19 +327,32 @@ class Store { } // Apply range/pagination if provided - if ($range !== null && $range->type() === RangeType::TALLY) { - // For TALLY ranges, use position (skip) and tally (limit) - /** @var IRangeTally $rangeTally */ - $rangeTally = $range; - $findOptions['skip'] = $rangeTally->getPosition(); - $findOptions['limit'] = $rangeTally->getTally(); + if ($range !== null) { + if ($range->type() === RangeType::TALLY) { + // For TALLY ranges, use position (skip) and tally (limit) + /** @var \KTXF\Resource\Range\IRangeTally $rangeTally */ + $rangeTally = $range; + $findOptions['skip'] = $rangeTally->getPosition(); + $findOptions['limit'] = $rangeTally->getTally(); + } elseif ($range->type() === RangeType::DATE) { + // For DATE ranges, filter by date fields + /** @var \KTXF\Resource\Range\IRangeDate $rangeDate */ + $rangeDate = $range; + $query['data.startsOn'] = [ + '\$gte' => $rangeDate->getStart()->format('c'), + '\$lte' => $rangeDate->getEnd()->format('c') + ]; + } } $cursor = $this->_store->selectCollection($this->_EntityTable)->find($query, $findOptions); $list = []; foreach ($cursor as $entry) { - $entity = (new Entity())->fromStore($entry); - $list[$entity->id()] = $entity; + $entity = $this->entityFresh()->fromStore($entry); + $identifier = $entity->identifier(); + if ($identifier !== null) { + $list[(string) $identifier] = $entity; + } } return $list; } @@ -381,7 +402,7 @@ class Store { * @param string $collection collection identifier * @param string ...$identifiers entity identifiers (eid UUID strings) * - * @return array + * @return array */ public function entityFetch(string $tenantId, string $userId, string $collectionId, string ...$identifiers): array { // Query for entities using eid field @@ -394,8 +415,11 @@ class Store { $list = []; foreach ($cursor as $entry) { - $entity = (new Entity())->fromStore($entry); - $list[$entity->id()] = $entity; + $entity = $this->entityFresh()->fromStore($entry); + $identifier = $entity->identifier(); + if ($identifier !== null) { + $list[(string) $identifier] = $entity; + } } return $list; @@ -406,21 +430,12 @@ class Store { * * @since Release 1.0.0 * - * @return Entity + * @return EntityResource */ - public function entityFresh(): Entity { - return new Entity(); + public function entityFresh(): EntityResource { + return new EntityResource(); } - /** - * create a entity entry in the data store - * - * @since Release 1.0.0 - * - * @param Entity $entity entity to create - * - * @return Entity - */ /** * create a entity entry in the data store * @@ -428,11 +443,11 @@ class Store { * * @param string $userId user identifier * @param string $collection collection identifier - * @param Entity $entity entity to create + * @param EntityResource $entity entity to create * - * @return Entity + * @return EntityResource */ - public function entityCreate(string $tenantId, string $userId, string $collectionId, Entity $entity): Entity { + public function entityCreate(string $tenantId, string $userId, string $collectionId, EntityResource $entity): EntityResource { // convert entity to store format $data = $entity->toStore(); // assign identifiers and timestamps @@ -449,7 +464,7 @@ class Store { if ($result->getInsertedCount() === 1) { $eid = $data['eid']; - $entity->fromStore(['eid' => $eid, 'tid' => $tenantId, 'uid' => $userId, 'cid' => $collectionId]); + $entity = $this->entityFresh()->fromStore($data); // Chronicle the creation (operation 1) $this->chronicleDocument($tenantId, $collectionId, $eid, 1); } @@ -465,11 +480,11 @@ class Store { * @param string $userId user identifier * @param string $collection collection identifier * @param string $identifier entity identifier - * @param Entity $entity entity to modify + * @param EntityResource $entity entity to modify * - * @return Entity + * @return EntityResource */ - public function entityModify(string $tenantId, string $userId, string $collectionId, string $identifier, Entity $entity): Entity { + public function entityModify(string $tenantId, string $userId, string $collectionId, string $identifier, EntityResource $entity): EntityResource { // convert entity to store format $data = $entity->toStore(); $data['modifiedOn'] = date('c'); @@ -497,23 +512,26 @@ class Store { * * @param string $userId user identifier * @param string $collection collection identifier - * @param Entity $entity entity to delete + * @param EntityResource $entity entity to delete * - * @return Entity + * @return EntityResource */ - public function entityDestroy(string $tenantId, string $userId, string $collectionId, Entity $entity): Entity { - $identifier = $entity->id(); + public function entityDestroy(string $tenantId, string $userId, string $collectionId, EntityResource $entity): EntityResource { + $identifier = $entity->identifier(); + if ($identifier === null) { + return $entity; + } $result = $this->_store->selectCollection($this->_EntityTable)->deleteOne([ 'tid' => $tenantId, 'uid' => $userId, 'cid' => $collectionId, - 'eid' => $identifier + 'eid' => (string) $identifier ]); if ($result->getDeletedCount() === 1) { // Chronicle the deletion (operation 3) - $this->chronicleDocument($tenantId, $collectionId, $identifier, 3); + $this->chronicleDocument($tenantId, $collectionId, (string) $identifier, 3); } return $entity; @@ -560,12 +578,19 @@ class Store { private function chronicleDocument(string $tid, string $cid, string $eid, int $operation): void { // retrieve current token from collection $collection = $this->_store->selectCollection($this->_CollectionTable)->findOne([ + 'tid' => $tid, 'cid' => $cid ], [ 'projection' => ['signature' => 1, '_id' => 0] ]); - - $signature = $collection['signature'] ?? 0; + + $signatureRaw = $collection['signature'] ?? 0; + if (is_numeric($signatureRaw)) { + $signature = (int)$signatureRaw; + } else { + $decoded = is_string($signatureRaw) ? base64_decode($signatureRaw, true) : false; + $signature = (is_string($decoded) && is_numeric($decoded)) ? (int)$decoded : 0; + } // document operation in chronicle $this->_store->selectCollection($this->_ChronicleTable)->insertOne([ @@ -577,10 +602,10 @@ class Store { 'mutatedOn' => time(), ]); - // increment token atomically + // update signature as normalized numeric value $this->_store->selectCollection($this->_CollectionTable)->updateOne( - ['cid' => $cid], - ['$inc' => ['signature' => 1]] + ['tid' => $tid, 'cid' => $cid], + ['$set' => ['signature' => $signature + 1]] ); } -- 2.39.5