From 644c83e898f5147d9eb061640b72c174f8a07745 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Wed, 25 Feb 2026 00:16:08 -0500 Subject: [PATCH] refactor: standardize protocol Signed-off-by: Sebastian Krupinski --- lib/Controllers/DefaultController.php | 783 +++++++++++++++++--------- lib/Manager.php | 658 +++++++++++++--------- 2 files changed, 925 insertions(+), 516 deletions(-) diff --git a/lib/Controllers/DefaultController.php b/lib/Controllers/DefaultController.php index 458d317..1d33a73 100644 --- a/lib/Controllers/DefaultController.php +++ b/lib/Controllers/DefaultController.php @@ -14,357 +14,622 @@ use KTXC\Http\Response\JsonResponse; use KTXC\SessionIdentity; use KTXC\SessionTenant; use KTXF\Controller\ControllerAbstract; -use KTXF\People\Collection\ICollectionBase; use KTXF\Resource\Selector\SourceSelector; use KTXF\Routing\Attributes\AuthenticatedRoute; use KTXM\PeopleManager\Manager; +use Psr\Log\LoggerInterface; +use Throwable; class DefaultController extends ControllerAbstract { + private const ERR_MISSING_PROVIDER = 'Missing parameter: provider'; + private const ERR_MISSING_IDENTIFIER = 'Missing parameter: identifier'; + private const ERR_MISSING_SERVICE = 'Missing parameter: service'; + private const ERR_MISSING_COLLECTION = 'Missing parameter: collection'; + private const ERR_MISSING_DATA = 'Missing parameter: data'; + private const ERR_MISSING_SOURCES = 'Missing parameter: sources'; + private const ERR_MISSING_IDENTIFIERS = 'Missing parameter: identifiers'; + private const ERR_INVALID_OPERATION = 'Invalid operation: '; + private const ERR_INVALID_PROVIDER = 'Invalid parameter: provider must be a string'; + private const ERR_INVALID_SERVICE = 'Invalid parameter: service must be a string'; + private const ERR_INVALID_IDENTIFIER = 'Invalid parameter: identifier must be a string'; + private const ERR_INVALID_COLLECTION = 'Invalid parameter: collection must be a string or integer'; + private const ERR_INVALID_SOURCES = 'Invalid parameter: sources must be an array'; + private const ERR_INVALID_IDENTIFIERS = 'Invalid parameter: identifiers must be an array'; + private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array'; + public function __construct( private readonly SessionTenant $tenantIdentity, private readonly SessionIdentity $userIdentity, - private Manager $peopleManager, + private readonly Manager $manager, + private readonly LoggerInterface $logger, ) {} - + /** - * Retrieve list of available providers - * - * @return JsonResponse - */ - #[AuthenticatedRoute('/v1', name: 'peoplemanager.v1', methods: ['POST'])] + * Main API endpoint for mail operations + * + * Single operation: + * { + * "version": 1, + * "transaction": "tx-1", + * "operation": "entity.create", + * "data": {...} + * } + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/v1', name: 'people.manager.v1', methods: ['POST'])] + public function index( + int $version, + string $transaction, + string|null $operation = null, + array|null $data = null, + string|null $user = null + ): JsonResponse { - public function index(int $version, string $transaction, string $operation, array $data = [], string|null $user = null): JsonResponse { + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); - // authorize request - $tenantId = $this->tenantIdentity->identifier(); - $userId = $this->userIdentity->identifier(); + try { + + if ($operation !== null) { + $result = $this->processOperation($tenantId, $userId, $operation, $data ?? [], []); + return new JsonResponse([ + 'version' => $version, + 'transaction' => $transaction, + 'operation' => $operation, + 'status' => 'success', + 'data' => $result + ], JsonResponse::HTTP_OK); + } + + throw new InvalidArgumentException('Operation must be provided'); + + } catch (Throwable $t) { + $this->logger->error('Error processing request', ['exception' => $t]); + return new JsonResponse([ + 'version' => $version, + 'transaction' => $transaction, + 'operation' => $operation, + 'status' => 'error', + 'data' => [ + 'code' => $t->getCode(), + 'message' => $t->getMessage() + ] + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } - try { - $data = $this->process($tenantId, $userId, $operation, $data); - return new JsonResponse([ - 'version' => $version, - 'transaction' => $transaction, - 'operation' => $operation, - 'status' => 'success', - 'data' => $data, - ], JsonResponse::HTTP_OK); - } catch (\Throwable $t) { - return new JsonResponse([ - 'version' => $version, - 'transaction' => $transaction, - 'operation' => $operation, - 'status' => 'error', - 'data' => [ - 'code' => $t->getCode(), - 'message' => $t->getMessage(), - ] - ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); - } - } + /** + * Process a single operation + */ + private function processOperation(string $tenantId, string $userId, string $operation, array $data): mixed { + return match ($operation) { + // Provider operations + 'provider.list' => $this->providerList($tenantId, $userId, $data), + 'provider.fetch' => $this->providerFetch($tenantId, $userId, $data), + 'provider.extant' => $this->providerExtant($tenantId, $userId, $data), + + // Service operations + 'service.list' => $this->serviceList($tenantId, $userId, $data), + 'service.fetch' => $this->serviceFetch($tenantId, $userId, $data), + 'service.extant' => $this->serviceExtant($tenantId, $userId, $data), + 'service.create' => $this->serviceCreate($tenantId, $userId, $data), + 'service.update' => $this->serviceUpdate($tenantId, $userId, $data), + 'service.delete' => $this->serviceDelete($tenantId, $userId, $data), + 'service.test' => $this->serviceTest($tenantId, $userId, $data), + + // Collection operations + 'collection.list' => $this->collectionList($tenantId, $userId, $data), + 'collection.fetch' => $this->collectionFetch($tenantId, $userId, $data), + 'collection.extant' => $this->collectionExtant($tenantId, $userId, $data), + 'collection.create' => $this->collectionCreate($tenantId, $userId, $data), + 'collection.update' => $this->collectionUpdate($tenantId, $userId, $data), + 'collection.delete' => $this->collectionDelete($tenantId, $userId, $data), + + // Entity operations + 'entity.list' => $this->entityList($tenantId, $userId, $data), + 'entity.fetch' => $this->entityFetch($tenantId, $userId, $data), + 'entity.extant' => $this->entityExtant($tenantId, $userId, $data), + 'entity.create' => $this->entityCreate($tenantId, $userId, $data), + 'entity.update' => $this->entityUpdate($tenantId, $userId, $data), + 'entity.delete' => $this->entityDelete($tenantId, $userId, $data), + 'entity.delta' => $this->entityDelta($tenantId, $userId, $data), + 'entity.move' => throw new InvalidArgumentException('Operation not implemented: ' . $operation), + 'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation), + + default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation) + }; + } - private function process(string $tenantId, string $userId, string $operation, array $data): mixed { + // ==================== Provider Operations ==================== - return match ($operation) { - 'provider.list' => $this->providerList($tenantId, $userId, $data), - 'provider.extant' => $this->providerExtant($tenantId, $userId, $data), - 'service.list' => $this->serviceList($tenantId, $userId, $data), - 'service.extant' => $this->serviceExtant($tenantId, $userId, $data), - 'service.fetch' => $this->serviceFetch($tenantId, $userId, $data), - 'collection.list' => $this->collectionList($tenantId, $userId, $data), - 'collection.extant' => $this->collectionExtant($tenantId, $userId, $data), - 'collection.fetch' => $this->collectionFetch($tenantId, $userId, $data), - 'collection.create' => $this->collectionCreate($tenantId, $userId, $data), - 'collection.modify' => $this->collectionModify($tenantId, $userId, $data), - 'collection.destroy' => $this->collectionDestroy($tenantId, $userId, $data), - 'entity.list' => $this->entityList($tenantId, $userId, $data), - 'entity.delta' => $this->entityDelta($tenantId, $userId, $data), - 'entity.extant' => $this->entityExtant($tenantId, $userId, $data), - 'entity.fetch' => $this->entityFetch($tenantId, $userId, $data), - 'entity.create' => $this->entityCreate($tenantId, $userId, $data), - 'entity.modify' => $this->entityModify($tenantId, $userId, $data), - 'entity.destroy' => $this->entityDestroy($tenantId, $userId, $data), - default => throw new InvalidArgumentException("Invalid operation: $operation"), - }; + private function providerList(string $tenantId, string $userId, array $data): mixed { - } - - // ==================== Provider Operations ==================== - - private function providerList(string $tenantId, string $userId, array $data): mixed { - - $sources = null; + $sources = null; if (isset($data['sources']) && is_array($data['sources'])) { $sources = new SourceSelector(); $sources->jsonDeserialize($data['sources']); } - return $this->peopleManager->providerList($tenantId, $userId, $sources); + return $this->manager->providerList($tenantId, $userId, $sources); - } + } - private function providerExtant(string $tenantId, string $userId, array $data): mixed { + private function providerFetch(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); + } + + return $this->manager->providerFetch($tenantId, $userId, $data['identifier']); + } - if (!isset($data['sources']) || !is_array($data['sources'])) { - throw new InvalidArgumentException('Invalid sources selector provided'); + private function providerExtant(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); } $sources = new SourceSelector(); $sources->jsonDeserialize($data['sources']); - return $this->peopleManager->providerExtant($tenantId, $userId, $sources); - - } + return $this->manager->providerExtant($tenantId, $userId, $sources); - // ==================== Service Operations ===================== + } - private function serviceList(string $tenantId, string $userId, array $data): mixed { - $sources = null; - if (isset($data['sources']) && is_array($data['sources'])) { - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); + // ==================== Service Operations ===================== + + private function serviceList(string $tenantId, string $userId, array $data): mixed { + + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + + return $this->manager->serviceList($tenantId, $userId, $sources); + + } + + private function serviceFetch(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); + } + + return $this->manager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']); + } - return $this->peopleManager->serviceList($tenantId, $userId, $sources); - - } - - private function serviceExtant(string $tenantId, string $userId, array $data): mixed { - - if (!isset($data['sources']) || !is_array($data['sources'])) { - throw new InvalidArgumentException('Invalid sources selector provided'); + private function serviceExtant(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); } $sources = new SourceSelector(); $sources->jsonDeserialize($data['sources']); - - return $this->peopleManager->serviceExtant($tenantId, $userId, $sources); - } + return $this->manager->serviceExtant($tenantId, $userId, $sources); + } - private function serviceFetch(string $tenantId, string $userId, array $data): mixed { + private function serviceCreate(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['data'])) { + throw new InvalidArgumentException(self::ERR_MISSING_DATA); + } + if (!is_array($data['data'])) { + throw new InvalidArgumentException(self::ERR_INVALID_DATA); + } + + return $this->manager->serviceCreate( + $tenantId, + $userId, + $data['provider'], + $data['data'] + ); + } - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['identifier']) || !is_string($data['identifier'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - - return $this->peopleManager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']); + private function serviceUpdate(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); + } + if (!isset($data['data'])) { + throw new InvalidArgumentException(self::ERR_MISSING_DATA); + } + if (!is_array($data['data'])) { + throw new InvalidArgumentException(self::ERR_INVALID_DATA); + } + + return $this->manager->serviceUpdate( + $tenantId, + $userId, + $data['provider'], + $data['identifier'], + $data['data'] + ); + } - } + private function serviceDelete(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); + } + + return $this->manager->serviceDelete( + $tenantId, + $userId, + $data['provider'], + $data['identifier'] + ); + } - private function collectionList(string $tenantId, string $userId, array $data): mixed { + private function serviceTest(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + + if (!isset($data['identifier']) && !isset($data['location']) && !isset($data['identity'])) { + throw new InvalidArgumentException('Either a service identifier or location and identity must be provided for service test'); + } + + return $this->manager->serviceTest( + $tenantId, + $userId, + $data['provider'], + $data['identifier'] ?? null, + $data['location'] ?? null, + $data['identity'] ?? null, + ); + } - $sources = null; - if (isset($data['sources']) && is_array($data['sources'])) { - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); - } - $filter = $data['filter'] ?? null; - $sort = $data['sort'] ?? null; - // retrieve collections - return $this->peopleManager->collectionList($tenantId, $userId, $sources, $filter, $sort); + // ==================== Collection Operations ==================== - } + private function collectionList(string $tenantId, string $userId, array $data): mixed { + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + + $filter = $data['filter'] ?? null; + $sort = $data['sort'] ?? null; + + return $this->manager->collectionList($tenantId, $userId, $sources, $filter, $sort); + } - private function collectionExtant(string $tenantId, string $userId, array $data = []): mixed { + private function collectionExtant(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); + } + + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + + return $this->manager->collectionExtant($tenantId, $userId, $sources); + } - if (!isset($data['sources']) || !is_array($data['sources'])) { - throw new InvalidArgumentException('Invalid sources selector provided'); - } - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); - // retrieve collection status - return $this->peopleManager->collectionExtant($tenantId, $userId, $sources); + private function collectionFetch(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['service'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SERVICE); + } + if (!is_string($data['service'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier']) && !is_int($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); + } + + return $this->manager->collectionFetch( + $tenantId, + $userId, + $data['provider'], + $data['service'], + $data['identifier'] + ); + } - } + private function collectionCreate(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['service'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SERVICE); + } + if (!is_string($data['service'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); + } + if (isset($data['collection']) && !is_string($data['collection']) && !is_int($data['collection'])) { + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); + } + if (!isset($data['properties'])) { + throw new InvalidArgumentException(self::ERR_MISSING_DATA); + } + if (!is_array($data['properties'])) { + throw new InvalidArgumentException(self::ERR_INVALID_DATA); + } + + return $this->manager->collectionCreate( + $tenantId, + $userId, + $data['provider'], + $data['service'], + $data['collection'] ?? null, + $data['properties'] + ); + } - private function collectionFetch(string $tenantId, string $userId, array $data = []): mixed { + private function collectionUpdate(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['service'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SERVICE); + } + if (!is_string($data['service'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier']) && !is_int($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); + } + if (!isset($data['properties'])) { + throw new InvalidArgumentException(self::ERR_MISSING_DATA); + } + if (!is_array($data['properties'])) { + throw new InvalidArgumentException(self::ERR_INVALID_DATA); + } + + return $this->manager->collectionUpdate( + $tenantId, + $userId, + $data['provider'], + $data['service'], + $data['identifier'], + $data['properties'] + ); + } - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - if (!isset($data['identifier'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); - } - // retrieve collection - return $this->peopleManager->collectionFetch($tenantId, $userId, $data['provider'], $data['service'], $data['identifier']); + private function collectionDelete(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['service'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SERVICE); + } + if (!is_string($data['service'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); + } + if (!is_string($data['identifier']) && !is_int($data['identifier'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); + } + + return $this->manager->collectionDelete( + $tenantId, + $userId, + $data['provider'], + $data['service'], + $data['identifier'], + $data['options'] ?? [] + ); + } - } + // ==================== Entity Operations ==================== - private function collectionCreate(string $tenantId, string $userId, array $data = []): mixed { - - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - if (!isset($data['data'])) { - throw new InvalidArgumentException('Invalid collection data provided'); - } - $options = $data['options'] ?? []; - // create collection - return $this->peopleManager->collectionCreate($tenantId, $userId, $data['provider'], $data['service'], $data['data'], $options); - - } - - private function collectionModify(string $tenantId, string $userId, array $data = []): mixed { - - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - if (!isset($data['identifier'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); - } - if (!isset($data['data'])) { - throw new InvalidArgumentException('Invalid collection data provided'); - } - // modify collection - return $this->peopleManager->collectionModify($tenantId, $userId, $data['provider'], $data['service'], $data['identifier'], $data['data']); - - } - - private function collectionDestroy(string $tenantId, string $userId, array $data = []): mixed { - - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - if (!isset($data['identifier'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); - } - // destroy collection - return ['success' => $this->peopleManager->collectionDestroy($tenantId, $userId, $data['provider'], $data['service'], $data['identifier'])]; - - } - - private function entityList(string $tenantId, string $userId, array $data = []): mixed { - - $sources = null; - if (isset($data['sources']) && is_array($data['sources'])) { - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); - } + private function entityList(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); + } + + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + $filter = $data['filter'] ?? null; $sort = $data['sort'] ?? null; $range = $data['range'] ?? null; - // retrieve entities - return $this->peopleManager->entityList($tenantId, $userId, $sources, $filter, $sort, $range); - } + return $this->manager->entityList($tenantId, $userId, $sources, $filter, $sort, $range); - private function entityDelta(string $tenantId, string $userId, array $data = []): mixed { + } - if (!isset($data['sources']) || !is_array($data['sources'])) { - throw new InvalidArgumentException('Invalid sources selector provided'); - } - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); - // retrieve entity delta - return $this->peopleManager->entityDelta($tenantId, $userId, $sources); + private function entityFetch(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['provider'])) { + throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); + } + if (!is_string($data['provider'])) { + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); + } + if (!isset($data['service'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SERVICE); + } + if (!is_string($data['service'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); + } + if (!isset($data['collection'])) { + throw new InvalidArgumentException(self::ERR_MISSING_COLLECTION); + } + if (!is_string($data['collection']) && !is_int($data['collection'])) { + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); + } + if (!isset($data['identifiers'])) { + throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIERS); + } + if (!is_array($data['identifiers'])) { + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIERS); + } + + return $this->manager->entityFetch( + $tenantId, + $userId, + $data['provider'], + $data['service'], + $data['collection'], + $data['identifiers'] + ); + } - } - - private function entityExtant(string $tenantId, string $userId, array $data = []): mixed { - - if (!isset($data['sources']) || !is_array($data['sources'])) { - throw new InvalidArgumentException('Invalid sources selector provided'); - } - $sources = new SourceSelector(); - $sources->jsonDeserialize($data['sources']); - // retrieve entity status - return $this->peopleManager->entityExtant($tenantId, $userId, $sources); - - } - - private function entityFetch(string $tenantId, string $userId, array $data = []): mixed { - - if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); - } - if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); - } - if (!isset($data['collection'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); - } - if (!isset($data['identifiers']) || !is_array($data['identifiers'])) { - throw new InvalidArgumentException('Invalid entity identifiers provided'); - } - // retrieve entities - return $this->peopleManager->entityFetch($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifiers']); - - } + private function entityExtant(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); + } + + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + + return $this->manager->entityExtant($tenantId, $userId, $sources); + } private function entityCreate(string $tenantId, string $userId, array $data = []): mixed { if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); } if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); } if (!isset($data['collection'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); - } - if (!isset($data['data']) || !is_array($data['data'])) { - throw new InvalidArgumentException('Invalid entity data provided'); + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); } + $properties = $data['properties'] ?? $data['data'] ?? null; + if (!is_array($properties)) { + throw new InvalidArgumentException('Invalid parameter: properties must be an array'); + } $options = $data['options'] ?? []; - // create entity - return $this->peopleManager->entityCreate($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['data'], $options); + + return $this->manager->entityCreate($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $properties, $options); } - private function entityModify(string $tenantId, string $userId, array $data = []): mixed { + private function entityUpdate(string $tenantId, string $userId, array $data = []): mixed { if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); } if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); } if (!isset($data['collection'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); } if (!isset($data['identifier'])) { - throw new InvalidArgumentException('Invalid entity identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); } - if (!isset($data['data']) || !is_array($data['data'])) { - throw new InvalidArgumentException('Invalid entity data provided'); - } - // modify entity - return $this->peopleManager->entityModify($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier'], $data['data']); + $properties = $data['properties'] ?? $data['data'] ?? null; + if (!is_array($properties)) { + throw new InvalidArgumentException('Invalid parameter: properties must be an array'); + } + + return $this->manager->entityUpdate($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier'], $properties); } - private function entityDestroy(string $tenantId, string $userId, array $data = []): mixed { + private function entityDelete(string $tenantId, string $userId, array $data = []): mixed { if (!isset($data['provider']) || !is_string($data['provider'])) { - throw new InvalidArgumentException('Invalid provider identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); } if (!isset($data['service']) || !is_string($data['service'])) { - throw new InvalidArgumentException('Invalid service identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_SERVICE); } if (!isset($data['collection'])) { - throw new InvalidArgumentException('Invalid collection identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_COLLECTION); } if (!isset($data['identifier'])) { - throw new InvalidArgumentException('Invalid entity identifier provided'); + throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); } - // destroy entity - return ['success' => $this->peopleManager->entityDestroy($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier'])]; + + return $this->manager->entityDelete($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier']); } -} + private function entityDelta(string $tenantId, string $userId, array $data): mixed { + if (!isset($data['sources'])) { + throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); + } + if (!is_array($data['sources'])) { + throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); + } + + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + + return $this->manager->entityDelta($tenantId, $userId, $sources); + } + +} \ No newline at end of file diff --git a/lib/Manager.php b/lib/Manager.php index cb46c32..8dd86fb 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -6,12 +6,16 @@ namespace KTXM\PeopleManager; use InvalidArgumentException; use KTXC\Resource\ProviderManager; -use KTXF\People\Collection\ICollectionBase; -use KTXF\People\Entity\IEntityBase; -use KTXF\People\Provider\IProviderBase; -use KTXF\People\Service\IServiceBase; -use KTXF\People\Service\IServiceCollectionMutable; -use KTXF\People\Service\IServiceEntityMutable; +use KTXF\People\Collection\CollectionBaseInterface; +use KTXF\People\Collection\CollectionMutableInterface; +use KTXF\People\Entity\EntityBaseInterface; +use KTXF\People\Entity\EntityMutableInterface; +use KTXF\People\Provider\ProviderBaseInterface; +use KTXF\People\Provider\ProviderServiceMutateInterface; +use KTXF\People\Service\ServiceBaseInterface; +use KTXF\People\Service\ServiceCollectionMutableInterface; +use KTXF\People\Service\ServiceEntityMutableInterface; +use KTXF\Resource\Filter\IFilter; use KTXF\Resource\Provider\ProviderInterface; use KTXF\Resource\Range\RangeAnchorType; use KTXF\Resource\Range\RangeType; @@ -19,6 +23,7 @@ use KTXF\Resource\Selector\CollectionSelector; use KTXF\Resource\Selector\EntitySelector; use KTXF\Resource\Selector\ServiceSelector; use KTXF\Resource\Selector\SourceSelector; +use KTXF\Resource\Sort\ISort; use Psr\Log\LoggerInterface; class Manager { @@ -33,13 +38,32 @@ class Manager { * * @param SourceSelector|null $sources collection of provider identifiers * - * @return array collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider] + * @return array collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider] */ public function providerList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { // determine filter from sources $filter = ($sources !== null && $sources->identifiers() !== []) ? $sources->identifiers() : null; // retrieve providers from provider manager - return $this->providerManager->providers(ProviderInterface::TYPE_PEOPLE, $filter); + return $this->providerManager->providers(ProviderBaseInterface::TYPE_PEOPLE, $filter); + } + + /** + * Retrieve specific provider for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $provider provider identifier + * + * @return ProviderBaseInterface + * @throws InvalidArgumentException + */ + public function providerFetch(string $tenantId, string $userId, string $provider): ProviderBaseInterface { + // retrieve provider + $providers = $this->providerList($tenantId, $userId, new SourceSelector([$provider => true])); + if (!isset($providers[$provider])) { + throw new InvalidArgumentException("Provider '$provider' not found"); + } + return $providers[$provider]; } /** @@ -62,25 +86,6 @@ class Manager { return $responseData; } - /** - * Retrieve specific provider for specific user - * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string $provider provider identifier - * - * @return IProviderBase - * @throws InvalidArgumentException - */ - public function providerFetch(string $tenantId, string $userId, string $provider): IProviderBase { - // retrieve provider - $providers = $this->providerList($tenantId, $userId, new SourceSelector([$provider => true])); - if (!isset($providers[$provider])) { - throw new InvalidArgumentException("Provider '$provider' not found"); - } - return $providers[$provider]; - } - /** * Retrieve available services for specific user * @@ -88,7 +93,7 @@ class Manager { * @param string $userId user identifier * @param SourceSelector|null $sources list of provider and service identifiers * - * @return array> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]] + * @return array> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]] */ public function serviceList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { // retrieve providers @@ -96,13 +101,34 @@ class Manager { // retrieve services for each provider $responseData = []; foreach ($providers as $provider) { - $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : []; $services = $provider->serviceList($tenantId, $userId, $serviceFilter); - $responseData[$provider->id()] = $services; + $responseData[$provider->identifier()] = $services; } return $responseData; } + /** + * Retrieve service for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * + * @return ServiceBaseInterface + * @throws InvalidArgumentException + */ + public function serviceFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId): ServiceBaseInterface { + // retrieve provider and service + $service = $this->providerFetch($tenantId, $userId, $providerId)->serviceFetch($tenantId, $userId, $serviceId); + if ($service === null) { + throw new InvalidArgumentException("Service '$serviceId' not found for provider '$providerId'"); + } + // retrieve services + return $service; + } + /** * Confirm which services are available * @@ -123,44 +149,132 @@ class Manager { // retrieve services for each available provider foreach ($providers as $provider) { - $serviceSelector = $sources[$provider->id()]; + $serviceSelector = $sources[$provider->identifier()]; $serviceAvailability = $provider->serviceExtant($tenantId, $userId, ...$serviceSelector->identifiers()); - $responseData[$provider->id()] = $serviceAvailability; + $responseData[$provider->identifier()] = $serviceAvailability; } return $responseData; } /** - * Retrieve service for specific user + * Create a new service * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string $providerId provider identifier - * @param string|int $serviceId service identifier - * - * @return IServiceBase - * @throws InvalidArgumentException + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param string $providerId Provider identifier + * @param array $data Service configuration data + * + * @return ServiceBaseInterface Created service + * + * @throws InvalidArgumentException If provider doesn't support service creation */ - public function serviceFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId): IServiceBase { + public function serviceCreate(string $tenantId, ?string $userId, string $providerId, array $data): ServiceBaseInterface { // retrieve provider and service - $service = $this->providerFetch($tenantId, $userId, $providerId)->serviceFetch($tenantId, $userId, $serviceId); - if ($service === null) { - throw new InvalidArgumentException("Service '$serviceId' not found for provider '$providerId'"); + $provider = $this->providerFetch($tenantId, $userId, $providerId); + if ($provider instanceof ProviderServiceMutateInterface === false) { + throw new InvalidArgumentException("Provider '$providerId' does not support service creation"); } - // retrieve services - return $service; + + // Create a fresh service instance + $service = $provider->serviceFresh(); + + // Deserialize the data into the service + $service->jsonDeserialize($data); + + // Create the service + $serviceId = $provider->serviceCreate($tenantId, $userId, $service); + + // Fetch and return the created service + return $provider->serviceFetch($tenantId, $userId, $serviceId); } /** - * Retrieve available collections for specific user + * Update an existing service * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param SourceSelector $sources list of provider and service identifiers - * - * @return array> collections of available services e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string $userId User identifier for context + * @param string $providerId Provider identifier + * @param string|int $serviceId Service identifier + * @param array $data Updated service configuration data + * + * @return ServiceBaseInterface Updated service + * + * @throws InvalidArgumentException If provider doesn't support service modification or service not found */ - public function collectionList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null): array { + public function serviceUpdate(string $tenantId, string $userId, string $providerId, string|int $serviceId, array $data): ServiceBaseInterface { + // retrieve provider and service + $provider = $this->providerFetch($tenantId, $userId, $providerId); + if ($provider instanceof ProviderServiceMutateInterface === false) { + throw new InvalidArgumentException("Provider '$providerId' does not support service modification"); + } + + // Fetch existing service + $service = $provider->serviceFetch($tenantId, $userId, $serviceId); + if ($service === null) { + throw new InvalidArgumentException("Service '$serviceId' not found"); + } + + // Update with new data + $service->jsonDeserialize($data); + + // Modify the service + $provider->serviceModify($tenantId, $userId, $service); + + // Fetch and return the updated service + return $provider->serviceFetch($tenantId, $userId, $serviceId); + } + + /** + * Delete a service + * + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string $userId User identifier for context + * @param string $providerId Provider identifier + * @param string|int $serviceId Service identifier + * + * @return bool True if service was deleted + * + * @throws InvalidArgumentException If provider doesn't support service deletion or service not found + */ + public function serviceDelete(string $tenantId, string $userId, string $providerId, string|int $serviceId): bool { + // retrieve provider and service + $provider = $this->providerFetch($tenantId, $userId, $providerId); + if ($provider instanceof ProviderServiceMutateInterface === false) { + throw new InvalidArgumentException("Provider '$providerId' does not support service creation"); + } + + // Fetch existing service + $service = $provider->serviceFetch($tenantId, $userId, $serviceId); + if ($service === null) { + throw new InvalidArgumentException("Service '$serviceId' not found"); + } + + // Delete the service + return $provider->serviceDestroy($tenantId, $userId, $service); + } + + // ==================== Collection Operations ==================== + + /** + * List collections across services + * + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param SourceSelector|null $sources Provider/service sources + * @param IFilter|null $filter Collection filter + * @param ISort|null $sort Collection sort + * + * @return array>> Collections grouped by provider/service + */ + public function collectionList(string $tenantId, ?string $userId, ?SourceSelector $sources = null, ?IFilter $filter = null, ?ISort $sort = null): array { // confirm that sources are provided if ($sources === null) { $sources = new SourceSelector([]); @@ -170,7 +284,8 @@ class Manager { // retrieve services for each provider $responseData = []; foreach ($providers as $provider) { - $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : []; + /** @var ServiceBaseInterface[] $services */ $services = $provider->serviceList($tenantId, $userId, $serviceFilter); // retrieve collections for each service foreach ($services as $service) { @@ -190,9 +305,9 @@ class Manager { $collectionSort->condition($attribute, $direction); } } - $collections = $service->collectionList($collectionFilter, $collectionSort); + $collections = $service->collectionList('', $collectionFilter, $collectionSort); if ($collections !== []) { - $responseData[$provider->id()][$service->id()] = $collections; + $responseData[$provider->identifier()][$service->identifier()] = $collections; } } } @@ -200,15 +315,17 @@ class Manager { } /** - * Confirm which collections are available + * Check if collections exist * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param SourceSelector $sources collection of provider and service identifiers to confirm - * - * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => true, 'collection2' => false]]]] + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param SourceSelector $sources Collection sources with identifiers + * + * @return array>> Existence map grouped by provider/service */ - public function collectionExtant(string $tenantId, string $userId, SourceSelector $sources): array { + public function collectionExtant(string $tenantId, ?string $userId, SourceSelector $sources): array { // retrieve available providers $providers = $this->providerList($tenantId, $userId, $sources); $providersRequested = $sources->identifiers(); @@ -219,19 +336,20 @@ class Manager { // check services and collections for each available provider foreach ($providers as $provider) { - $serviceSelector = $sources[$provider->id()]; + $serviceSelector = $sources[$provider->identifier()]; $servicesRequested = $serviceSelector->identifiers(); + /** @var ServiceBaseInterface[] $servicesAvailable */ $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); // mark unavailable services as false if ($servicesUnavailable !== []) { - $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + $responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false); } // confirm collections for each available service foreach ($servicesAvailable as $service) { - $collectionSelector = $serviceSelector[$service->id()]; + $collectionSelector = $serviceSelector[$service->identifier()]; $collectionsRequested = $collectionSelector->identifiers(); if ($collectionsRequested === []) { @@ -239,29 +357,36 @@ class Manager { } // check each requested collection - foreach ($collectionsRequested as $collectionId) { - $responseData[$provider->id()][$service->id()][$collectionId] = $service->collectionExtant((string)$collectionId); - } + $collectionsAvailable = $service->collectionExtant(...$collectionsRequested); + $collectionsUnavailable = array_diff($collectionsRequested, array_keys($collectionsAvailable)); + $responseData[$provider->identifier()][$service->identifier()] = array_merge( + $collectionsAvailable, + array_fill_keys($collectionsUnavailable, false) + ); } } return $responseData; } /** - * Retrieve collection for specific user + * Fetch a specific collection * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string $providerId provider identifier - * @param string|int $serviceId service identifier - * @param string|int $collectionId collection identifier - * - * @return ICollectionBase - * @throws InvalidArgumentException + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param string $providerId Provider identifier + * @param string|int $serviceId Service identifier + * @param string|int $collectionId Collection identifier + * + * @return CollectionBaseInterface|null */ - public function collectionFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ICollectionBase { - // retrieve services + public function collectionFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ?CollectionBaseInterface { + // retrieve service $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + if ($service === null) { + return null; + } // retrieve collection return $service->collectionFetch($collectionId); } @@ -273,30 +398,34 @@ class Manager { * @param string $userId user identifier * @param string $providerId provider identifier * @param string|int $serviceId service identifier - * @param ICollectionBase|array $collection collection to create + * @param string|int|null $collectionId collection identifier (parent collection) + * @param CollectionMutableInterface|array $object collection to create * @param array $options additional options for creation * - * @return ICollectionBase + * @return CollectionBaseInterface * @throws InvalidArgumentException */ - public function collectionCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, ICollectionBase|array $collection, array $options = []): ICollectionBase { + public function collectionCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int|null $collectionId, CollectionMutableInterface|array $object, array $options = []): CollectionBaseInterface { // retrieve service $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); // Check if service supports collection creation - if (!($service instanceof IServiceCollectionMutable)) { + if (!($service instanceof ServiceCollectionMutableInterface)) { throw new InvalidArgumentException("Service does not support collection mutations"); } - if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_CREATE)) { + if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_CREATE)) { throw new InvalidArgumentException("Service is not capable of creating collections"); } - if (is_array($collection)) { - $collection = $service->collectionFresh()->jsonDeserialize($collection); + if (is_array($object)) { + $collection = $service->collectionFresh(); + $collection->getProperties()->jsonDeserialize($object); + } else { + $collection = $object; } - // Create collection (location is empty string for root) - return $service->collectionCreate('', $collection, $options); + // Create collection + return $service->collectionCreate($collectionId, $collection, $options); } /** @@ -307,89 +436,100 @@ class Manager { * @param string $providerId provider identifier * @param string|int $serviceId service identifier * @param string|int $collectionId collection identifier - * @param ICollectionBase $collectionData collection with modifications + * @param CollectionMutableInterface|array $object collection to modify * - * @return ICollectionBase + * @return CollectionBaseInterface * @throws InvalidArgumentException */ - public function collectionModify(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, ICollectionBase|array $collectionData): ICollectionBase { + public function collectionUpdate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, CollectionMutableInterface|array $object): CollectionBaseInterface { // retrieve service $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); - // Check if service supports collection modification - if (!($service instanceof IServiceCollectionMutable)) { + // Check if service supports collection creation + if (!($service instanceof ServiceCollectionMutableInterface)) { throw new InvalidArgumentException("Service does not support collection mutations"); } - if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_MODIFY)) { - throw new InvalidArgumentException("Service is not capable of modifying collections"); + if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_UPDATE)) { + throw new InvalidArgumentException("Service is not capable of updating collections"); } - if (is_array($collectionData)) { - $collectionData = $service->collectionFresh()->jsonDeserialize($collectionData); - } - - // Modify collection - return $service->collectionModify($collectionId, $collectionData); - } - /** - * Delete a collection for a specific user - * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string $providerId provider identifier - * @param string|int $serviceId service identifier - * @param string|int $collectionId collection identifier - * - * @return bool - * @throws InvalidArgumentException - */ - public function collectionDestroy(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): bool { - // retrieve service - $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); - - // Check if service supports collection destruction - if (!($service instanceof IServiceCollectionMutable)) { - throw new InvalidArgumentException("Service does not support collection mutations"); - } - if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_DESTROY)) { - throw new InvalidArgumentException("Service is not capable of destroying collections"); + if (is_array($object)) { + $collection = $service->collectionFresh(); + $collection->getProperties()->jsonDeserialize($object); + } else { + $collection = $object; } - // Destroy collection and cast result to bool - return (bool)$service->collectionDestroy($collectionId); + // Update collection + return $service->collectionUpdate($collectionId, $collection); } + /** + * Delete a specific collection + * + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param string $providerId Provider identifier + * @param string|int $serviceId Service identifier + * @param string|int $collectionId Collection identifier + * + * @return CollectionBaseInterface|null + */ + public function collectionDelete(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $options = []): bool { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports collection deletion + if (!($service instanceof ServiceCollectionMutableInterface)) { + throw new InvalidArgumentException("Service does not support collection mutations"); + } + if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_DELETE)) { + throw new InvalidArgumentException("Service is not capable of deleting collections"); + } + + $force = $options['force'] ?? false; + $recursive = $options['recursive'] ?? false; + + // delete collection + return $service->collectionDelete($collectionId, $force, $recursive); + } + + // ==================== Entity Operations ==================== /** - * Retrieve available entities for specific user + * List Entities in a collection * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param SourceSelector $sources list of provider and service identifiers - * - * @return array> collections of store enteties e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string $userId User identifier + * @param SourceSelector $sources Entity sources with collection identifiers + * @param array|null $filter Entity filter + * @param array|null $sort Entity sort + * @param array|null $range Entity range/pagination + * + * @return array>>> Entities grouped by provider/service/collection */ - public function entityList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?array $range = null): array { - // confirm that sources are provided - if ($sources === null) { - $sources = new SourceSelector([]); - } + public function entityList(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): array { // retrieve providers $providers = $this->providerList($tenantId, $userId, $sources); // retrieve services for each provider $responseData = []; foreach ($providers as $provider) { // retrieve services for each provider - $serviceSelector = $sources[$provider->id()]; + $serviceSelector = $sources[$provider->identifier()]; $servicesSelected = $provider->serviceList($tenantId,$userId, $serviceSelector->identifiers()); + /** @var ServiceBaseInterface $service */ foreach ($servicesSelected as $service) { // retrieve collections for each service - $collectionSelector = $serviceSelector[$service->id()]; + $collectionSelector = $serviceSelector[$service->identifier()]; $collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; if ($collectionSelected === []) { - $collections = $service->collectionList(); + $collections = $service->collectionList(''); $collectionSelected = array_map( - fn($collection) => $collection->id(), + fn($collection) => $collection->identifier(), $collections ); } @@ -437,67 +577,45 @@ class Manager { if ($entities === []) { continue; } - $responseData[$provider->id()][$service->id()][$collectionId] = $entities; + $responseData[$provider->identifier()][$service->identifier()][$collectionId] = $entities; } } - } + } + return $responseData; } - - /** - * Confirm which entities are available + + /** + * Fetch specific messages * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param array $sources collection of provider and service identifiers to confirm - * - * @return array collection of providers and their availability status + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param string $providerId Provider identifier + * @param string|int $serviceId Service identifier + * @param string|int $collectionId Collection identifier + * @param array $identifiers Message identifiers + * + * @return array Messages indexed by ID */ - public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { - // confirm that sources are provided - if ($sources === null) { - $sources = new SourceSelector([]); - } - // retrieve providers - $providers = $this->providerList($tenantId, $userId, $sources); - $providersRequested = $sources->identifiers(); - $providersUnavailable = array_diff($providersRequested, array_keys($providers)); - // initialize response with unavailable providers - $responseData = array_fill_keys($providersUnavailable, false); - // iterate through available providers - foreach ($providers as $provider) { - $serviceSelector = $sources[$provider->id()]; - $servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : []; - $services = $provider->serviceList($tenantId, $userId, $servicesRequested); - $servicesUnavailable = array_diff($servicesRequested, array_keys($services)); - if ($servicesUnavailable !== []) { - $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); - } - // iterate through available services - foreach ($services as $service) { - $collectionSelector = $serviceSelector[$service->id()]; - $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; - if ($collectionsRequested === []) { - $responseData[$provider->id()][$service->id()] = false; - continue; - } - foreach ($collectionsRequested as $collection) { - $entitySelector = $collectionSelector[$collection] ?? null; - $responseData[$provider->id()][$service->id()][$collection] = $service->entityDelta($collection, $entitySelector); - } - } - } - return $responseData; + public function entityFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array { + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // retrieve collection + return $service->entityFetch($collectionId, ...$identifiers); } /** - * Confirm which entities are available + * Check if messages exist * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param SourceSelector $sources collection of provider and service identifiers to confirm - * - * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => ['entity1' => true, 'entity2' => false]]]] + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param SourceSelector $sources Message sources with identifiers + * + * @return array>>> Existence map grouped by provider/service/collection */ public function entityExtant(string $tenantId, string $userId, SourceSelector $sources): array { // confirm that sources are provided @@ -514,19 +632,20 @@ class Manager { // check services, collections, and entities for each available provider foreach ($providers as $provider) { - $serviceSelector = $sources[$provider->id()]; + $serviceSelector = $sources[$provider->identifier()]; $servicesRequested = $serviceSelector->identifiers(); + /** @var ServiceBaseInterface[] $servicesAvailable */ $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); // mark unavailable services as false if ($servicesUnavailable !== []) { - $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + $responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false); } // check collections and entities for each available service foreach ($servicesAvailable as $service) { - $collectionSelector = $serviceSelector[$service->id()]; + $collectionSelector = $serviceSelector[$service->identifier()]; $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; if ($collectionsRequested === []) { @@ -540,7 +659,7 @@ class Manager { if (!$collectionExists) { // collection doesn't exist, mark as false - $responseData[$provider->id()][$service->id()][$collectionId] = false; + $responseData[$provider->identifier()][$service->identifier()][$collectionId] = false; continue; } @@ -550,10 +669,10 @@ class Manager { // handle both array of entity IDs and boolean true (meaning check if collection exists) if ($entitySelector instanceof EntitySelector) { // check specific entities within the collection - $responseData[$provider->id()][$service->id()][$collectionId] = $service->entityExtant($collectionId, ...$entitySelector->identifiers()); + $responseData[$provider->identifier()][$service->identifier()][$collectionId] = $service->entityExtant($collectionId, ...$entitySelector->identifiers()); } elseif ($entitySelector === true) { // just checking if collection exists (already confirmed above) - $responseData[$provider->id()][$service->id()][$collectionId] = true; + $responseData[$provider->identifier()][$service->identifier()][$collectionId] = true; } } } @@ -562,104 +681,132 @@ class Manager { } /** - * Retrieve entity for specific user and collection + * Get message delta/changes * - * @param string $tenantId tenant identifier - * @param string $userId user identifier - * @param string $providerId provider identifier - * @param string|int $serviceId service identifier - * @param string|int $collectionId collection identifier - * @param array $identifiers entity identifiers - * - * @return array + * @since 2025.05.01 + * + * @param string $tenantId Tenant identifier + * @param string|null $userId User identifier for context + * @param SourceSelector $sources Message sources with signatures + * + * @return array>> Delta grouped by provider/service/collection */ - public function entityFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array { - // retrieve services - $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); - // retrieve collection - return $service->entityFetch($collectionId, ...$identifiers); + public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + // iterate through available providers + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->identifier()]; + $servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : []; + /** @var ServiceBaseInterface[] $services */ + $services = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($services)); + if ($servicesUnavailable !== []) { + $responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false); + } + // iterate through available services + foreach ($services as $service) { + $collectionSelector = $serviceSelector[$service->identifier()]; + $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + if ($collectionsRequested === []) { + $responseData[$provider->identifier()][$service->identifier()] = false; + continue; + } + foreach ($collectionsRequested as $collection) { + $entitySelector = $collectionSelector[$collection] ?? null; + $responseData[$provider->identifier()][$service->identifier()][$collection] = $service->entityDelta($collection, $entitySelector); + } + } + } + return $responseData; } /** - * Create a new entity in a specific collection for a specific user + * Create a new entity in a collection * * @param string $tenantId tenant identifier * @param string $userId user identifier * @param string $providerId provider identifier * @param string|int $serviceId service identifier * @param string|int $collectionId collection identifier - * @param IEntityBase|array $entity entity to create - * @param array $options additional options for creation + * @param EntityMutableInterface|array $entity entity to create + * @param array $options additional options * - * @return IEntityBase + * @return EntityBaseInterface * @throws InvalidArgumentException */ - public function entityCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, IEntityBase|array $entity, array $options = []): IEntityBase { + public function entityCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, EntityMutableInterface|array $object, array $options = []): EntityBaseInterface { // retrieve service $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); // Check if service supports entity creation - if (!($service instanceof IServiceEntityMutable)) { + if (!($service instanceof ServiceEntityMutableInterface)) { throw new InvalidArgumentException("Service does not support entity mutations"); } - if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_CREATE)) { + if (!$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_CREATE)) { throw new InvalidArgumentException("Service is not capable of creating entities"); } - if (is_array($entity)) { - $entityInstance = $service->entityFresh(); - $entityInstance->jsonDeserialize($entity); + if (is_array($object)) { + $entity = $service->entityFresh(); + $entity->getProperties()->jsonDeserialize($object); } else { - $entityInstance = $entity; + $entity = $object; } - // Create entity - return $service->entityCreate($collectionId, $entityInstance, $options); + return $service->entityCreate($collectionId, $entity, $options); } /** - * Modify an existing entity in a collection for a specific user + * Modify an existing entity in a collection * * @param string $tenantId tenant identifier * @param string $userId user identifier - * @param string $providerId provider identifier + * @param string|int $providerId provider identifier * @param string|int $serviceId service identifier * @param string|int $collectionId collection identifier * @param string|int $identifier entity identifier - * @param IEntityBase|array $entity entity with modifications + * @param EntityBaseInterface|array $entity entity with modifications * - * @return IEntityBase + * @return EntityBaseInterface * @throws InvalidArgumentException */ - public function entityModify(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier, IEntityBase|array $entity): IEntityBase { + public function entityUpdate(string $tenantId, string $userId, string|int $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier, EntityBaseInterface|array $object): EntityBaseInterface { // retrieve service $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); - // Check if service supports entity modification - if (!($service instanceof IServiceEntityMutable)) { + // Check if service supports entity creation + if (!($service instanceof ServiceEntityMutableInterface)) { throw new InvalidArgumentException("Service does not support entity mutations"); } - if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_MODIFY)) { - throw new InvalidArgumentException("Service is not capable of modifying entities"); - } - - if (is_array($entity)) { - $entityInstance = $service->entityFresh(); - $entityInstance->jsonDeserialize($entity); - } else { - $entityInstance = $entity; + if (!$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_CREATE)) { + throw new InvalidArgumentException("Service is not capable of creating entities"); } - // Modify entity - return $service->entityModify($collectionId, $identifier, $entityInstance); + if (is_array($object)) { + $entity = $service->entityFresh(); + $entity->getProperties()->jsonDeserialize($object); + } else { + $entity = $object; + } + + return $service->entityUpdate($collectionId, $identifier, $entity); } /** - * Delete an entity from a collection for a specific user + * Destroy an entity from a collection * * @param string $tenantId tenant identifier * @param string $userId user identifier - * @param string $providerId provider identifier + * @param string|int $providerId provider identifier * @param string|int $serviceId service identifier * @param string|int $collectionId collection identifier * @param string|int $identifier entity identifier @@ -667,19 +814,16 @@ class Manager { * @return bool * @throws InvalidArgumentException */ - public function entityDestroy(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier): bool { - // retrieve service + public function entityDelete(string $tenantId, string $userId, string|int $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier): bool { $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); - - // Check if service supports entity destruction - if (!($service instanceof IServiceEntityMutable)) { - throw new InvalidArgumentException("Service does not support entity mutations"); - } - if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_DESTROY)) { - throw new InvalidArgumentException("Service is not capable of destroying entities"); + + if (!($service instanceof ServiceEntityMutableInterface)) { + throw new InvalidArgumentException('Service does not support entity destruction'); } - // Destroy entity and cast result to bool - return (bool)$service->entityDestroy($collectionId, $identifier); + $entity = $service->entityDelete($collectionId, $identifier); + + return $entity !== null; } + } \ No newline at end of file -- 2.39.5