* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ChronoProviderLocal\Providers\Personal; use KTXF\Chrono\Collection\ICollectionMutable; use KTXF\Chrono\Service\IServiceBase; use KTXF\Chrono\Entity\IEntityMutable; use KTXF\Chrono\Service\IServiceCollectionMutable; use KTXF\Chrono\Service\IServiceEntityMutable; 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\ChronoProviderLocal\Store\Personal\Store; class PersonalService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable { protected const SERVICE_ID = 'personal'; protected const SERVICE_LABEL = 'Personal Calendar Service'; protected const SERVICE_PROVIDER = 'default'; protected array $serviceCollectionCache = []; 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 => [ self::CAPABILITY_FILTER_ANY => 's:100:256:771', self::CAPABILITY_FILTER_ID => 'a:10:64:192', self::CAPABILITY_FILTER_URID => 'a:10:64:192', self::CAPABILITY_FILTER_LABEL => 's:100:256:771', self::CAPABILITY_FILTER_DESCRIPTION => 's:100:256:771', ], self::CAPABILITY_COLLECTION_LIST_SORT => [ self::CAPABILITY_SORT_ID, self::CAPABILITY_SORT_URID, self::CAPABILITY_SORT_LABEL, self::CAPABILITY_SORT_PRIORITY ], 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 => [ self::CAPABILITY_FILTER_ANY => 's:100:256:771', self::CAPABILITY_FILTER_ID => 'a:10:64:192', self::CAPABILITY_FILTER_URID => 'a:10:64:192', self::CAPABILITY_FILTER_LABEL => 's:100:256:771', ], self::CAPABILITY_ENTITY_LIST_SORT => [ self::CAPABILITY_SORT_LABEL, ], self::CAPABILITY_ENTITY_LIST_RANGE => [ self::CAPABILITY_RANGE_TALLY => [self::CAPABILITY_RANGE_TALLY_ABSOLUTE, self::CAPABILITY_RANGE_TALLY_RELATIVE], self::CAPABILITY_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_MODIFY => true, self::CAPABILITY_ENTITY_DESTROY => true, self::CAPABILITY_ENTITY_COPY => true, self::CAPABILITY_ENTITY_MOVE => 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 { $this->serviceTenantId = $tenantId; $this->serviceUserId = $userId; return $this; } 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 in(): string{ return self::SERVICE_PROVIDER; } public function id(): string { return self::SERVICE_ID; } public function getLabel(): string { return (string)self::SERVICE_LABEL; } public function getEnabled(): bool { return (bool)$this->serviceEnabled; } public function collectionList(?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 $id): bool { // determine if collection is cached if (isset($this->serviceCollectionCache[$id])) { return true; } // retrieve from store return $this->store->collectionExtant($this->serviceTenantId, $this->serviceUserId, $id); } public function collectionFetch(string|int $id): ?Collection { // determine if collection is cached if (isset($this->serviceCollectionCache[$id])) { return $this->serviceCollectionCache[$id]; } // retrieve from store $collection = $this->store->collectionFetch($this->serviceTenantId, $this->serviceUserId, $id); if ($collection !== null) { $this->serviceCollectionCache[$collection->id()] = $collection; return $collection; } return null; } public function collectionFresh(): Collection { return new Collection(); } public function collectionCreate(string|int $location, ICollectionMutable $collection, array $options): Collection { // 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); $this->serviceCollectionCache[$result->id()] = $result; return $result; } public function collectionModify(string|int $id, ICollectionMutable $collection): Collection { // 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(); $nativeCollection->jsonDeserialize($collection->jsonSerialize()); } else { $nativeCollection = clone $collection; } // modify collection in store $result = $this->store->collectionModify($this->serviceTenantId, $this->serviceUserId, $nativeCollection); return $result; } public function collectionDestroy(string|int $id): 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'): 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 delta from store return $this->store->chronicleReminisce($this->serviceTenantId, $this->serviceUserId,$collection, $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, IEntityMutable $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 entityModify(string|int $collection, string|int $identifier, IEntityMutable $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 entityDestroy(string|int $collection, string|int $identifier): IEntityMutable { // 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; } }