* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderLocalChrono\Providers\Personal; use KTXF\Chrono\Collection\CollectionBaseInterface; use KTXF\Chrono\Collection\CollectionMutableInterface; use KTXF\Chrono\Entity\EntityMutableInterface; use KTXF\Chrono\Service\ServiceBaseInterface; use KTXF\Chrono\Service\ServiceCollectionMutableInterface; use KTXF\Chrono\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; use KTXF\Resource\Range\IRange; use KTXF\Resource\Range\RangeDate; use KTXF\Resource\Range\RangeTally; use KTXF\Resource\Range\RangeType; use KTXF\Resource\Sort\ISort; use KTXF\Resource\Sort\Sort; use KTXM\ProviderLocalChrono\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 array $serviceCollectionCache = []; private ?string $serviceTenantId = null; private ?string $serviceUserId = null; private ?bool $serviceEnabled = true; private array $serviceAbilities = [ self::CAPABILITY_COLLECTION_LIST => true, self::CAPABILITY_COLLECTION_LIST_FILTER => [ self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:100:256:256', ], self::CAPABILITY_COLLECTION_LIST_SORT => [ 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_UPDATE => true, self::CAPABILITY_COLLECTION_DELETE => true, self::CAPABILITY_ENTITY_LIST => true, self::CAPABILITY_ENTITY_LIST_FILTER => [ 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 => [ self::CAPABILITY_ENTITY_SORT_ID, self::CAPABILITY_ENTITY_SORT_URID, ], self::CAPABILITY_ENTITY_LIST_RANGE => [ self::CAPABILITY_ENTITY_RANGE_TALLY => [ self::CAPABILITY_ENTITY_RANGE_TALLY_ABSOLUTE, self::CAPABILITY_ENTITY_RANGE_TALLY_RELATIVE ], self::CAPABILITY_ENTITY_RANGE_DATE => true, ], self::CAPABILITY_ENTITY_DELTA => true, self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, self::CAPABILITY_ENTITY_CREATE => true, self::CAPABILITY_ENTITY_UPDATE => true, self::CAPABILITY_ENTITY_DELETE => true, ]; public function __construct( private Store $store, ) {} 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]; } return false; } public function capabilities(): array { return $this->serviceAbilities; } public function provider(): string { return self::PROVIDER_IDENTIFIER; } public function identifier(): string { return self::SERVICE_IDENTIFIER; } public function getLabel(): string { return (string)self::SERVICE_LABEL; } public function getEnabled(): bool { return (bool)$this->serviceEnabled; } 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 ?? []; } public function collectionListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); } public function collectionListSort(): Sort { return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } 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; } } } 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; } public function collectionFresh(): Collection { return new 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(); $nativeCollection->jsonDeserialize($collection->jsonSerialize()); } else { $nativeCollection = clone $collection; } // create collection in store $result = $this->store->collectionCreate($this->serviceTenantId, $this->serviceUserId, $nativeCollection); $resultIdentifier = $result->identifier(); if ($resultIdentifier !== null) { $this->serviceCollectionCache[(string) $resultIdentifier] = $result; } 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"); } // validate ownership if ($this->collectionExtant($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 Collection)) { $nativeCollection = new Collection(); $data = $collection->jsonSerialize(); $data[Collection::JSON_PROPERTY_IDENTIFIER] = $id; $nativeCollection->jsonDeserialize($data); } else { $nativeCollection = clone $collection; if ($nativeCollection->identifier() === null) { $data = $nativeCollection->jsonSerialize(); $data[Collection::JSON_PROPERTY_IDENTIFIER] = $id; $nativeCollection->jsonDeserialize($data); } } // modify collection in store $result = $this->store->collectionModify($this->serviceTenantId, $this->serviceUserId, $nativeCollection); 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"); } // validate ownership if ($this->collectionExtant($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; } 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)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // 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}'"); } // retrieve entities from store $entries = $this->store->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range, $options); return $entries ?? []; } public function entityListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } public function entityListSort(): Sort { return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []); } public function entityListRange(RangeType $type): IRange { // validate type 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"); } public function entityExtant(string|int $collection, string|int ...$identifiers): array { // validate id if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // 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}'"); } // retrieve entity status from store return $this->store->entityExtant($collection, ...$identifiers); } 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"); } // 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}'"); } // retrieve entity delta from store $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 { // validate id if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' is not valid"); } // 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}'"); } // retrieve entity from store $entries = $this->store->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$identifiers); return $entries ?? []; } public function entityFresh(): Entity { return new Entity(); } public function entityCreate(string|int $collection, EntityMutableInterface $entity, array $options = []): Entity { // validate collection identifier if (!is_string($collection)) { throw new InvalidParameterException("Invalid: Collection identifier '$collection' 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}'"); } // convert entity to a native type if needed if (!($entity instanceof Entity)) { $nativeEntity = $this->entityFresh(); $nativeEntity->jsonDeserialize($entity->jsonSerialize()); } else { $nativeEntity = clone $entity; } // 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): Entity { // 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"); } // 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}'"); } // convert entity to a native type if needed if (!($entity instanceof Entity)) { $nativeEntity = $this->entityFresh(); $nativeEntity->jsonDeserialize($entity->jsonSerialize()); } else { $nativeEntity = clone $entity; } // 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"); } // 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; } }