diff --git a/lib/Providers/Personal/CollectionProperties.php b/lib/Providers/Personal/CollectionProperties.php index cf601a1..02d7a0e 100644 --- a/lib/Providers/Personal/CollectionProperties.php +++ b/lib/Providers/Personal/CollectionProperties.php @@ -21,29 +21,29 @@ class CollectionProperties extends CollectionPropertiesMutableAbstract { * Converts store document values into collection properties. */ public function fromStore(array $data): static { - $this->data[self::JSON_PROPERTY_CONTENT] = [ + $this->data[self::PROPERTY_CONTENT] = [ CollectionContent::Individual->value, CollectionContent::Organization->value, CollectionContent::Group->value ]; if (isset($data['label'])) { - $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['label']; + $this->data[self::PROPERTY_LABEL] = (string) $data['label']; } if (array_key_exists('description', $data)) { - $this->data[self::JSON_PROPERTY_DESCRIPTION] = $data['description']; + $this->data[self::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('rank', $data)) { + $this->data[self::PROPERTY_RANK] = $data['rank'] !== null ? (int) $data['rank'] : null; } if (array_key_exists('visibility', $data)) { - $this->data[self::JSON_PROPERTY_VISIBILITY] = $data['visibility'] !== null ? (bool) $data['visibility'] : null; + $this->data[self::PROPERTY_VISIBILITY] = $data['visibility'] !== null ? (bool) $data['visibility'] : null; } if (array_key_exists('color', $data)) { - $this->data[self::JSON_PROPERTY_COLOR] = $data['color']; + $this->data[self::PROPERTY_COLOR] = $data['color']; } if (isset($data['content'])) { - $this->data[self::JSON_PROPERTY_CONTENT] = (string) $data['content']; + $this->data[self::PROPERTY_CONTENT] = (string) $data['content']; } return $this; @@ -58,7 +58,7 @@ class CollectionProperties extends CollectionPropertiesMutableAbstract { return array_filter([ 'label' => $this->getLabel(), 'description' => $this->getDescription(), - 'priority' => $this->getPriority(), + 'rank' => $this->getRank(), 'visibility' => $this->getVisibility(), 'color' => $this->getColor(), 'content' => $content instanceof CollectionContent ? $content->value : null, diff --git a/lib/Providers/Personal/CollectionResource.php b/lib/Providers/Personal/CollectionResource.php index 4e90b56..57ead50 100644 --- a/lib/Providers/Personal/CollectionResource.php +++ b/lib/Providers/Personal/CollectionResource.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace KTXM\ProviderLocalPeople\Providers\Personal; use KTXF\People\Collection\CollectionMutableAbstract; +use KTXF\Resource\Identifier\CollectionIdentifier; class CollectionResource extends CollectionMutableAbstract { @@ -24,6 +25,14 @@ class CollectionResource extends CollectionMutableAbstract { parent::__construct($provider, $service); } + protected function nodeIdentifier(): CollectionIdentifier { + return new CollectionIdentifier( + $this->data[static::PROPERTY_PROVIDER], + $this->data[static::PROPERTY_SERVICE], + (string) $this->data[static::PROPERTY_IDENTIFIER] + ); + } + public function fromStore(array|object $data): self { if (is_object($data)) { @@ -31,23 +40,23 @@ class CollectionResource extends CollectionMutableAbstract { } if (isset($data['cid'])) { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['cid']; + $this->data[self::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']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['_id']; } elseif (is_array($data['_id']) && isset($data['_id']['$oid'])) { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']['$oid']; + $this->data[self::PROPERTY_IDENTIFIER] = (string) $data['_id']['$oid']; } else { - $this->data[self::JSON_PROPERTY_IDENTIFIER] = (string) $data['_id']; + $this->data[self::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->data[self::PROPERTY_CREATED] = $data['createdOn'] ?? $data['created'] ?? null; + $this->data[self::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; + $this->data[self::PROPERTY_SIGNATURE] = isset($data['signature']) ? md5((string) $data['signature']) : null; return $this; } @@ -61,12 +70,12 @@ class CollectionResource extends CollectionMutableAbstract { 'uuid' => $this->collectionUuid, 'label' => $properties->getLabel(), 'description' => $properties->getDescription(), - 'priority' => $properties->getPriority(), + 'rank' => $properties->getRank(), '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, + 'createdOn' => $this->data[self::PROPERTY_CREATED] ?? null, + 'modifiedOn' => $this->data[self::PROPERTY_MODIFIED] ?? null, + 'signature' => $this->data[self::PROPERTY_SIGNATURE] ?? null, 'enabled' => $this->collectionEnabled, ], static fn($value) => $value !== null); diff --git a/lib/Providers/Personal/EntityResource.php b/lib/Providers/Personal/EntityResource.php index afda1e3..666654a 100644 --- a/lib/Providers/Personal/EntityResource.php +++ b/lib/Providers/Personal/EntityResource.php @@ -30,14 +30,14 @@ class EntityResource extends EntityMutableAbstract { $document = (array) $document; } - $this->data[self::JSON_PROPERTY_IDENTIFIER] = $document['eid'] ?? null; + $this->data[self::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->data[self::PROPERTY_COLLECTION] = $document['cid'] ?? null; + $this->data[self::PROPERTY_CREATED] = $document['createdOn'] ?? null; + $this->data[self::PROPERTY_MODIFIED] = $document['modifiedOn'] ?? null; $this->getProperties()->fromStore($document); - $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($this->getDataJson())); + $this->data[self::PROPERTY_SIGNATURE] = md5(json_encode($this->getDataJson())); $this->entityDataObject = null; return $this; @@ -49,7 +49,7 @@ class EntityResource extends EntityMutableAbstract { 'uid' => $this->userId, 'cid' => $this->collection(), 'eid' => $this->identifier(), - 'createdOn' => $this->data[self::JSON_PROPERTY_CREATED] ?? date('c'), + 'createdOn' => $this->data[self::PROPERTY_CREATED] ?? date('c'), 'modifiedOn' => date('c'), ], static fn($value) => $value !== null); @@ -85,7 +85,7 @@ class EntityResource extends EntityMutableAbstract { { $this->entityDataObject = null; $this->getProperties()->setDataRaw($value); - $this->data[self::JSON_PROPERTY_SIGNATURE] = md5(json_encode($value)); + $this->data[self::PROPERTY_SIGNATURE] = md5(json_encode($value)); return $this; } diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index 5958cae..c36439c 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -9,9 +9,11 @@ declare(strict_types=1); namespace KTXM\ProviderLocalPeople\Providers\Personal; +use Generator; use KTXF\People\Collection\CollectionBaseInterface; -use KTXF\People\Collection\CollectionMutableInterface; -use KTXF\People\Entity\EntityMutableInterface; +use KTXF\People\Collection\CollectionPropertiesBaseInterface; +use KTXF\People\Entity\EntityBaseInterface; +use KTXF\People\Entity\EntityPropertiesMutableInterface; use KTXF\People\Service\ServiceBaseInterface; use KTXF\People\Service\ServiceCollectionMutableInterface; use KTXF\People\Service\ServiceEntityMutableInterface; @@ -20,6 +22,10 @@ use KTXF\Resource\Delta\DeltaCollection; use KTXF\Resource\Exceptions\InvalidParameterException; use KTXF\Resource\Filter\Filter; use KTXF\Resource\Filter\IFilter; +use KTXF\Resource\Identifier\CollectionIdentifier; +use KTXF\Resource\Identifier\CollectionIdentifierInterface; +use KTXF\Resource\Identifier\EntityIdentifier; +use KTXF\Resource\Identifier\EntityIdentifierInterface; use KTXF\Resource\Range\IRange; use KTXF\Resource\Range\RangeTally; use KTXF\Resource\Range\RangeType; @@ -29,11 +35,9 @@ use KTXM\ProviderLocalPeople\Store\Personal\Store; class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { - public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; - private const PROVIDER_IDENTIFIER = 'default'; private const SERVICE_IDENTIFIER = 'personal'; - private const SERVICE_LABEL = 'Personal Calendar Service'; + private const SERVICE_LABEL = 'Personal Contacts Service'; protected array $serviceCollectionCache = []; protected ?string $serviceTenantId = null; @@ -75,8 +79,10 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, self::CAPABILITY_ENTITY_CREATE => true, - self::CAPABILITY_ENTITY_UPDATE => true, + self::CAPABILITY_ENTITY_MODIFY => true, self::CAPABILITY_ENTITY_DELETE => true, + self::CAPABILITY_ENTITY_MOVE => true, + self::CAPABILITY_ENTITY_COPY => true, ]; public function __construct( @@ -91,23 +97,20 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI 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 => [], + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER, + self::PROPERTY_LABEL => self::SERVICE_LABEL, + self::PROPERTY_ENABLED => $this->serviceEnabled, + self::PROPERTY_CAPABILITIES => $this->serviceAbilities, + self::PROPERTY_LOCATION => null, + self::PROPERTY_IDENTITY => null, + self::PROPERTY_AUXILIARY => [], ], fn($v) => $v !== null); } public function capable(string $value): bool { - if (isset($this->serviceAbilities[$value])) { - return (bool)$this->serviceAbilities[$value]; - } - return false; + return isset($this->serviceAbilities[$value]); } public function capabilities(): array { @@ -152,8 +155,6 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI 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; @@ -168,6 +169,23 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } + public function collectionFetch(string|int $identifier): ?CollectionBaseInterface { + // determine if collection is cached + if (isset($this->serviceCollectionCache[$identifier])) { + return $this->serviceCollectionCache[$identifier]; + } + // retrieve from store + $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); + if ($collection !== null) { + $collectionIdentifier = $collection->identifier(); + if ($collectionIdentifier !== null) { + $this->serviceCollectionCache[(string) $collectionIdentifier] = $collection; + } + return $collection; + } + return null; + } + public function collectionExtant(string|int $location, string|int ...$identifiers): array { $resolvedIdentifiers = $identifiers !== [] ? $identifiers : [$location]; $response = []; @@ -192,34 +210,31 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $response; } - public function collectionFetch(string|int $identifier): ?CollectionBaseInterface { - // determine if collection is cached - if (isset($this->serviceCollectionCache[$identifier])) { - return $this->serviceCollectionCache[$identifier]; - } - // retrieve from store - $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, (string)$identifier); - if ($collection !== null) { - $collectionIdentifier = $collection->identifier(); - if ($collectionIdentifier !== null) { - $this->serviceCollectionCache[(string) $collectionIdentifier] = $collection; - } - return $collection; - } - return null; + /** + * Determine whether a single collection exists and belongs to the current user. + * + * @param string|int $collection collection identifier + * + * @return bool + */ + private function collectionAccessible(string|int $collection): bool { + $response = $this->collectionExtant($collection); + return ($response[$collection] ?? false) === true; } public function collectionFresh(): CollectionResource { return new CollectionResource(); } - public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface { + public function collectionCreate(CollectionIdentifierInterface|null $target, CollectionPropertiesBaseInterface $properties, array $options = []): CollectionBaseInterface { // convert collection to a native type if needed - if (!($collection instanceof CollectionResource)) { - $nativeCollection = new CollectionResource(); - $nativeCollection->jsonDeserialize($collection->jsonSerialize()); + if (!($properties instanceof CollectionResource)) { + $nativeCollection = $this->collectionFresh(); + $nativeCollection->jsonDeserialize([ + CollectionResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeCollection = clone $collection; + $nativeCollection = clone $properties; } // create collection in store $result = $this->store->collectionCreate($this->serviceTenantId, $this->serviceUserId, $nativeCollection); @@ -231,26 +246,24 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $result; } - 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"); - } + public function collectionUpdate(CollectionIdentifierInterface $target, CollectionPropertiesBaseInterface $properties): CollectionBaseInterface { + $id = $target->collection(); // validate ownership - if ($this->collectionExtant($id) === false) { + if ($this->collectionAccessible($id) === false) { 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 CollectionResource)) { + if (!($properties instanceof CollectionResource)) { $nativeCollection = new CollectionResource(); - $data = $collection->jsonSerialize(); - $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; - $nativeCollection->jsonDeserialize($data); + $nativeCollection->jsonDeserialize([ + CollectionResource::PROPERTY_IDENTIFIER => $id, + CollectionResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeCollection = clone $collection; + $nativeCollection = clone $properties; if ($nativeCollection->identifier() === null) { $data = $nativeCollection->jsonSerialize(); - $data[CollectionResource::JSON_PROPERTY_IDENTIFIER] = $id; + $data[CollectionResource::PROPERTY_IDENTIFIER] = $id; $nativeCollection->jsonDeserialize($data); } } @@ -259,39 +272,48 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $result; } - 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"); - } + public function collectionDelete(CollectionIdentifierInterface $target, bool $force = false): CollectionBaseInterface | true { + $id = $target->collection(); // validate ownership - if ($this->collectionExtant($id) === false) { + if ($this->collectionAccessible($id) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$id' does not exist or does not belong to user '{$this->serviceUserId}'"); } // destroy collection in store - if ($this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id)) { - unset($this->serviceCollectionCache[$id]); - return true; - } - return false; + $this->store->collectionDestroyById($this->serviceTenantId, $this->serviceUserId, $id); + unset($this->serviceCollectionCache[$id]); + return true; } // Entity operations - public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $options = null): array { + public function entityListBulk(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array { // validate id if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entities from store - $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $options); + $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $properties); return $entries ?? []; } + public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator { + // validate id + if (!is_string($collection)) { + throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); + } + // validate ownership + if ($this->collectionAccessible($collection) === false) { + throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); + } + // retrieve entities from store and yield one by one + $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $properties); + yield from ($entries ?? []); + } + public function entityListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } @@ -305,9 +327,6 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI if ($type === RangeType::TALLY) { return new RangeTally(); } - if ($type === RangeType::DATE) { - return new RangeDate(); - } throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid"); } @@ -317,7 +336,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entity status from store @@ -330,7 +349,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // validate ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // retrieve entity delta from store @@ -344,57 +363,73 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI ); } - public function entityFetch(string|int $collection, string|int ...$identifiers): array { - // validate id - if (!is_string($collection)) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); + public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array { + // group identifiers by collection + $byCollection = []; + foreach ($identifiers as $identifier) { + $byCollection[$identifier->collection()][] = $identifier->entity(); } - // validate ownership - if ($this->collectionExtant($collection) === false) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); + $results = []; + foreach ($byCollection as $collection => $entityIds) { + // skip collections that don't exist or don't belong to user + if ($this->collectionAccessible($collection) === false) { + continue; + } + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$entityIds); + if ($entries) { + $results = array_merge($results, $entries); + } + } + return $results; + } + + public function entityFetchStream(EntityIdentifierInterface ...$identifiers): Generator { + // group identifiers by collection + $byCollection = []; + foreach ($identifiers as $identifier) { + $byCollection[$identifier->collection()][] = $identifier->entity(); + } + foreach ($byCollection as $collection => $entityIds) { + // skip collections that don't exist or don't belong to user + if ($this->collectionAccessible($collection) === false) { + continue; + } + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$entityIds); + if ($entries) { + yield from $entries; + } } - // retrieve entity from store - $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$identifiers); - return $entries ?? []; } public function entityFresh(): EntityResource { return new EntityResource(); } - 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"); - } + public function entityCreate(CollectionIdentifierInterface $target, EntityPropertiesMutableInterface $properties, array $options = []): EntityBaseInterface { + $collection = $target->collection(); // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { 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 EntityResource)) { + if (!($properties instanceof EntityResource)) { $nativeEntity = $this->entityFresh(); - $nativeEntity->jsonDeserialize($entity->jsonSerialize()); + $nativeEntity->jsonDeserialize([ + EntityResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeEntity = clone $entity; + $nativeEntity = clone $properties; } // create entity in store (store will handle userId and collection) $result = $this->store->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $nativeEntity); - return $result; } - 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"); - } - // validate entity identifier - if (!is_string($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } + public function entityModify(EntityIdentifierInterface $target, EntityPropertiesMutableInterface $properties): EntityBaseInterface { + $collection = $target->collection(); + $identifier = $target->entity(); // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { + if ($this->collectionAccessible($collection) === false) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); } // validate entity extant and ownership @@ -402,47 +437,103 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI 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 entity to a native type if needed - if (!($entity instanceof Entity)) { + if (!($properties instanceof EntityResource)) { $nativeEntity = $this->entityFresh(); - $nativeEntity->jsonDeserialize($entity->jsonSerialize()); + $nativeEntity->jsonDeserialize([ + EntityResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(), + ]); } else { - $nativeEntity = clone $entity; + $nativeEntity = clone $properties; } - // modify entity in store $result = $this->store->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $nativeEntity); - return $result; } - 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"); + public function entityDelete(EntityIdentifierInterface ...$targets): array { + $results = []; + foreach ($targets as $target) { + $key = (string) $target; + $collection = $target->collection(); + $identifier = $target->entity(); + // validate collection extant and ownership + if ($this->collectionAccessible($collection) === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target]; + continue; + } + // validate entity extant and ownership + $extant = $this->store->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); + if (!isset($extant[$identifier]) || $extant[$identifier] === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target]; + continue; + } + // destroy entity in store + $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); + $results[$key] = ['disposition' => 'deleted', 'destination' => null, 'mutation' => $target]; } - // validate entity identifier - if (!is_string($identifier)) { - throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); - } - // validate collection extant and ownership - if ($this->collectionExtant($collection) === false) { - throw new InvalidParameterException("Invalid: Collection identifier '$collection' does not exist or does not belong to user '{$this->serviceUserId}'"); - } - // validate entity extant and ownership - $extant = $this->store->entityExtant($this->serviceTenantId, $this->serviceUserId, $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}'"); - } - // fetch entity before destruction to return it - $entities = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - $entity = $entities[$identifier] ?? $this->entityFresh(); - - // destroy entity in store - $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $collection, $identifier); - - return $entity; + return $results; } - + + public function entityMove(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array { + return $this->entityRelocate($target, true, ...$sources); + } + + public function entityCopy(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array { + return $this->entityRelocate($target, false, ...$sources); + } + + /** + * Relocate (move or copy) entities into a target collection. + * + * The store assigns a fresh identifier to each relocated entity, so the + * mutation identifier reflects the entity's new location. + * + * @param CollectionIdentifierInterface $target destination collection identifier + * @param bool $remove true to move (delete the source), false to copy + * @param EntityIdentifierInterface ...$sources source entity identifiers + * + * @return array + */ + private function entityRelocate(CollectionIdentifierInterface $target, bool $remove, EntityIdentifierInterface ...$sources): array { + $results = []; + $disposition = $remove ? 'moved' : 'copied'; + $targetCollection = $target->collection(); + // validate target collection extant and ownership + $targetValid = $this->collectionAccessible($targetCollection); + foreach ($sources as $source) { + $key = (string) $source; + $sourceCollection = $source->collection(); + $identifier = $source->entity(); + // validate target collection + if ($targetValid === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // validate source collection extant and ownership + if ($this->collectionAccessible($sourceCollection) === false) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // fetch source entity (scoped to user; also confirms the entity exists) + $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier); + $entity = $entries[$identifier] ?? null; + if ($entity === null) { + $results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source]; + continue; + } + // create a copy in the target collection (store assigns a fresh identifier) + $created = $this->store->entityCreate($this->serviceTenantId, $this->serviceUserId, $targetCollection, $entity); + // remove the source entity when moving + if ($remove) { + $this->store->entityDestroyById($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier); + } + // construct destination and mutation identifiers + $destination = new CollectionIdentifier($this->provider(), $this->identifier(), $targetCollection); + $mutation = new EntityIdentifier($this->provider(), $this->identifier(), $targetCollection, (string) $created->identifier()); + $results[$key] = ['disposition' => $disposition, 'destination' => $destination, 'mutation' => $mutation]; + } + return $results; + } + } diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index 09ee577..1a2183a 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -16,7 +16,6 @@ use KTXM\ProviderLocalPeople\Providers\Personal\PersonalService; class Provider implements ProviderBaseInterface { - 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'; @@ -36,10 +35,10 @@ class Provider implements ProviderBaseInterface { 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, + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_LABEL => self::PROVIDER_LABEL, + self::PROPERTY_CAPABILITIES => $this->providerAbilities, ]; } @@ -82,7 +81,6 @@ class Provider implements ProviderBaseInterface { return $this->providerAbilities; } - protected function serviceInstancePersonal(string $tenantId, string $userId): PersonalService { $service = $this->container->get(PersonalService::class); $service->initialize($tenantId, $userId); diff --git a/lib/Store/Personal/Store.php b/lib/Store/Personal/Store.php index 695a917..0afcb22 100644 --- a/lib/Store/Personal/Store.php +++ b/lib/Store/Personal/Store.php @@ -257,7 +257,8 @@ class Store { ['tid' => $tenantId, 'uid' => $userId, 'cid' => $cid], ['$set' => $data] ); - return $entity; + // return the canonical post-update collection so provider/service and full state are populated + return $this->collectionFetch($tenantId, $userId, (string) $cid) ?? $entity; } /** @@ -501,8 +502,10 @@ class Store { // Chronicle the modification (operation 2) $this->chronicleDocument($tenantId, $collectionId, $identifier, 2); } - - return $entity; + + // return the canonical post-update entity so provider/service and full state are populated + $entries = $this->entityFetch($tenantId, $userId, $collectionId, $identifier); + return $entries[$identifier] ?? $entity; } /**