refactor: remove sources selector in backend

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-19 23:45:41 -04:00
parent af227f35f9
commit 9845262e62
2 changed files with 251 additions and 303 deletions
+80 -114
View File
@@ -24,9 +24,6 @@ use KTXF\Resource\Identifier\ResourceIdentifier;
use KTXF\Resource\Identifier\ResourceIdentifiers; use KTXF\Resource\Identifier\ResourceIdentifiers;
use KTXF\Resource\Identifier\ServiceIdentifier; use KTXF\Resource\Identifier\ServiceIdentifier;
use KTXF\Resource\Provider\ResourceServiceLocationInterface; use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Selector\CollectionSelector;
use KTXF\Resource\Selector\ServiceSelector;
use KTXF\Resource\Selector\SourceSelector;
use KTXF\Routing\Attributes\AuthenticatedRoute; use KTXF\Routing\Attributes\AuthenticatedRoute;
use KTXM\MailManager\Manager; use KTXM\MailManager\Manager;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -41,6 +38,7 @@ class DefaultController extends ControllerAbstract {
private const ERR_MISSING_SOURCES = 'Missing parameter: sources'; private const ERR_MISSING_SOURCES = 'Missing parameter: sources';
private const ERR_MISSING_TARGET = 'Missing parameter: target'; private const ERR_MISSING_TARGET = 'Missing parameter: target';
private const ERR_MISSING_TARGETS = 'Missing parameter: targets'; private const ERR_MISSING_TARGETS = 'Missing parameter: targets';
private const ERR_MISSING_SENDER = 'Missing parameter: sender';
private const ERR_INVALID_OPERATION = 'Invalid operation: '; private const ERR_INVALID_OPERATION = 'Invalid operation: ';
private const ERR_INVALID_PROVIDER = 'Invalid parameter: provider must be a string'; 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_SERVICE = 'Invalid parameter: service must be a string';
@@ -48,6 +46,7 @@ class DefaultController extends ControllerAbstract {
private const ERR_INVALID_SOURCES = 'Invalid parameter: sources must be an array'; private const ERR_INVALID_SOURCES = 'Invalid parameter: sources must be an array';
private const ERR_INVALID_TARGET = 'Invalid parameter: target must be an array'; private const ERR_INVALID_TARGET = 'Invalid parameter: target must be an array';
private const ERR_INVALID_TARGETS = 'Invalid parameter: targets must be an array'; private const ERR_INVALID_TARGETS = 'Invalid parameter: targets must be an array';
private const ERR_INVALID_SENDER = 'Invalid parameter: sender must be a string';
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array'; private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
public function __construct( public function __construct(
@@ -172,40 +171,48 @@ class DefaultController extends ControllerAbstract {
private function providerList(string $tenantId, string $userId, array $data): mixed { private function providerList(string $tenantId, string $userId, array $data): mixed {
$sources = null; if (isset($data['targets'])) {
if (isset($data['sources']) && is_array($data['sources'])) { if (!is_array($data['targets'])) {
$sources = new SourceSelector(); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
$sources->jsonDeserialize($data['sources']); }
}
foreach ($data['targets'] as $target) {
if (!is_string($target)) {
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
}
}
}
return $this->manager->providerList($tenantId, $userId, $sources); return $this->manager->providerList($tenantId, $userId, $data['targets'] ?? []);
} }
private function providerFetch(string $tenantId, string $userId, array $data): mixed { private function providerFetch(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['identifier'])) { if (!isset($data['target'])) {
throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER); throw new InvalidArgumentException(self::ERR_MISSING_TARGET);
} }
if (!is_string($data['identifier'])) { if (!is_string($data['target'])) {
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER); throw new InvalidArgumentException(self::ERR_INVALID_TARGET);
} }
return $this->manager->providerFetch($tenantId, $userId, $data['identifier']); return $this->manager->providerFetch($tenantId, $userId, $data['target']);
} }
private function providerExtant(string $tenantId, string $userId, array $data): mixed { private function providerExtant(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['sources'])) { if (!isset($data['targets'])) {
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
} }
if (!is_array($data['sources'])) {
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); foreach ($data['targets'] as $target) {
} if (!is_string($target)) {
$sources = new SourceSelector(); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
$sources->jsonDeserialize($data['sources']); }
}
return $this->manager->providerExtant($tenantId, $userId, $sources); return $this->manager->providerExtant($tenantId, $userId, $data['targets']);
} }
@@ -213,13 +220,17 @@ class DefaultController extends ControllerAbstract {
private function serviceList(string $tenantId, string $userId, array $data): mixed { private function serviceList(string $tenantId, string $userId, array $data): mixed {
$sources = null; $targets = null;
if (isset($data['sources']) && is_array($data['sources'])) { if (isset($data['targets']) && is_array($data['targets'])) {
$sources = new SourceSelector(); $targets = ResourceIdentifiers::fromArray($data['targets']);
$sources->jsonDeserialize($data['sources']); foreach ($targets as $target) {
if (!$target instanceof CollectionIdentifier && !$target instanceof ServiceIdentifier) {
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
}
}
} }
return $this->manager->serviceList($tenantId, $userId, $sources); return $this->manager->serviceList($tenantId, $userId, $targets);
} }
@@ -243,16 +254,20 @@ class DefaultController extends ControllerAbstract {
private function serviceExtant(string $tenantId, string $userId, array $data): mixed { private function serviceExtant(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['sources'])) { if (!isset($data['targets'])) {
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
} }
if (!is_array($data['sources'])) { if (!is_array($data['targets'])) {
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
} }
$sources = new SourceSelector(); $targets = ResourceIdentifiers::fromArray($data['targets']);
$sources->jsonDeserialize($data['sources']); foreach ($targets as $target) {
if (!$target instanceof ServiceIdentifier) {
return $this->manager->serviceExtant($tenantId, $userId, $sources); throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service identifiers');
}
}
return $this->manager->serviceExtant($tenantId, $userId, $targets);
} }
private function serviceCreate(string $tenantId, string $userId, array $data): mixed { private function serviceCreate(string $tenantId, string $userId, array $data): mixed {
@@ -404,14 +419,12 @@ class DefaultController extends ControllerAbstract {
private function collectionList(string $tenantId, string $userId, array $data): mixed { private function collectionList(string $tenantId, string $userId, array $data): mixed {
$sources = null; $sources = null;
if (isset($data['sources']) && is_array($data['sources'])) { if (isset($data['sources']) && is_array($data['sources'])) {
// TODO: Refactor to use identifiers directly
$sources = ResourceIdentifiers::fromArray($data['sources']); $sources = ResourceIdentifiers::fromArray($data['sources']);
foreach ($sources as $source) { foreach ($sources as $source) {
if (!$source instanceof CollectionIdentifier && !$source instanceof ServiceIdentifier) { if (!$source instanceof CollectionIdentifier && !$source instanceof ServiceIdentifier) {
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers'); throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
} }
} }
$sources = $this->createSourceSelectorFromIdentifiers($sources);
} }
$filter = $data['filter'] ?? null; $filter = $data['filter'] ?? null;
@@ -428,7 +441,6 @@ class DefaultController extends ControllerAbstract {
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
} }
// TODO: Refactor to use identifiers directly
$targetIdentifiers = ResourceIdentifiers::fromArray($data['targets']); $targetIdentifiers = ResourceIdentifiers::fromArray($data['targets']);
foreach ($targetIdentifiers as $targetIdentifier) { foreach ($targetIdentifiers as $targetIdentifier) {
if (!$targetIdentifier instanceof CollectionIdentifier) { if (!$targetIdentifier instanceof CollectionIdentifier) {
@@ -436,16 +448,11 @@ class DefaultController extends ControllerAbstract {
} }
} }
$list = []; $list = $this->manager->collectionFetch(
foreach ($targetIdentifiers as $targetIdentifier) { $tenantId,
$list[(string)$targetIdentifier] = $this->manager->collectionFetch( $userId,
$tenantId, $targetIdentifier
$userId, );
$targetIdentifier->provider(),
$targetIdentifier->service(),
$targetIdentifier->collection()
);
}
return $list; return $list;
} }
@@ -457,14 +464,12 @@ class DefaultController extends ControllerAbstract {
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
} }
// TODO: Refactor to use identifiers directly
$sources = ResourceIdentifiers::fromArray($data['targets']); $sources = ResourceIdentifiers::fromArray($data['targets']);
foreach ($sources as $source) { foreach ($sources as $source) {
if (!$source instanceof CollectionIdentifier) { if (!$source instanceof CollectionIdentifier) {
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers'); throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
} }
} }
$sources = $this->createSourceSelectorFromIdentifiers($sources);
return $this->manager->collectionExtant($tenantId, $userId, $sources); return $this->manager->collectionExtant($tenantId, $userId, $sources);
} }
@@ -611,8 +616,6 @@ class DefaultController extends ControllerAbstract {
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers'); throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers');
} }
} }
$sources = $this->createSourceSelectorFromIdentifiers($sources);
$filter = $data['filter'] ?? null; $filter = $data['filter'] ?? null;
$sort = $data['sort'] ?? null; $sort = $data['sort'] ?? null;
@@ -637,8 +640,6 @@ class DefaultController extends ControllerAbstract {
} }
} }
$sources = $this->createSourceSelectorFromIdentifiers($sources);
$filter = $data['filter'] ?? null; $filter = $data['filter'] ?? null;
$sort = $data['sort'] ?? null; $sort = $data['sort'] ?? null;
$range = $data['range'] ?? null; $range = $data['range'] ?? null;
@@ -696,31 +697,39 @@ class DefaultController extends ControllerAbstract {
} }
private function entityExtant(string $tenantId, string $userId, array $data): mixed { private function entityExtant(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['sources'])) { if (!isset($data['targets'])) {
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
} }
if (!is_array($data['sources'])) { if (!is_array($data['targets'])) {
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
} }
$sources = new SourceSelector(); $targets = ResourceIdentifiers::fromArray($data['targets']);
$sources->jsonDeserialize($data['sources']); foreach ($targets as $target) {
if (!$target instanceof CollectionIdentifier && !$target instanceof EntityIdentifier) {
return $this->manager->entityExtant($tenantId, $userId, $sources); throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection or provider:service:collection:entity identifiers');
}
}
return $this->manager->entityExtant($tenantId, $userId, $targets);
} }
private function entityDelta(string $tenantId, string $userId, array $data): mixed { private function entityDelta(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['sources'])) { if (!isset($data['targets'])) {
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES); throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
} }
if (!is_array($data['sources'])) { if (!is_array($data['targets'])) {
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES); throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
} }
$sources = new SourceSelector(); $targets = ResourceIdentifiers::fromArray($data['targets']);
$sources->jsonDeserialize($data['sources']); foreach ($targets as $target) {
if (!$target instanceof CollectionIdentifier && !$target instanceof EntityIdentifier) {
return $this->manager->entityDelta($tenantId, $userId, $sources); throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection or provider:service:collection:signature identifiers');
}
}
return $this->manager->entityDelta($tenantId, $userId, $targets);
} }
private function entityPatch(string $tenantId, string $userId, array $data): mixed { private function entityPatch(string $tenantId, string $userId, array $data): mixed {
@@ -891,47 +900,4 @@ class DefaultController extends ControllerAbstract {
return $results; return $results;
} }
private function createSourceSelectorFromIdentifiers(ResourceIdentifiers $identifiers): SourceSelector {
$sources = new SourceSelector();
foreach ($identifiers as $identifier) {
if (!$identifier instanceof ServiceIdentifier) {
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
}
$provider = $identifier->provider();
$service = $identifier->service();
if (!isset($sources[$provider])) {
$sources[$provider] = new ServiceSelector();
}
$serviceSelector = $sources[$provider];
if (!$serviceSelector instanceof ServiceSelector) {
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection selectors');
}
if ($identifier instanceof ServiceIdentifier && !$identifier instanceof CollectionIdentifier) {
$serviceSelector[$service] = true;
continue;
}
if (isset($serviceSelector[$service]) && $serviceSelector[$service] === true) {
continue;
}
if (!isset($serviceSelector[$service])) {
$serviceSelector[$service] = new CollectionSelector();
}
$collectionSelector = $serviceSelector[$service];
if (!$collectionSelector instanceof CollectionSelector) {
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection selectors');
}
$collectionSelector[$identifier->collection()] = true;
}
return $sources;
}
} }
+171 -189
View File
@@ -34,10 +34,6 @@ use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface; use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Range\RangeAnchorType; use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeType; use KTXF\Resource\Range\RangeType;
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 KTXF\Resource\Sort\ISort;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -56,15 +52,13 @@ class Manager {
/** /**
* Retrieve available providers * Retrieve available providers
* *
* @param SourceSelector|null $sources collection of provider identifiers * @param ResourceIdentifiers|null $sources collection of provider identifiers
* *
* @return array<string,ProviderBaseInterface> collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider] * @return array<string,ProviderBaseInterface> collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider]
*/ */
public function providerList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { public function providerList(string $tenantId, string $userId, array|null $targets = []): array {
// determine filter from sources
$filter = ($sources !== null && $sources->identifiers() !== []) ? $sources->identifiers() : null;
// retrieve providers from provider manager // retrieve providers from provider manager
return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $filter); return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $targets ?: null);
} }
/** /**
@@ -77,27 +71,27 @@ class Manager {
* @return ProviderBaseInterface * @return ProviderBaseInterface
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function providerFetch(string $tenantId, string $userId, string $provider): ProviderBaseInterface { public function providerFetch(string $tenantId, string $userId, string $target): ProviderBaseInterface {
// retrieve provider // retrieve provider
$providers = $this->providerList($tenantId, $userId, new SourceSelector([$provider => true])); $providers = $this->providerList($tenantId, $userId, [$target]);
if (!isset($providers[$provider])) { if (!isset($providers[$target])) {
throw new InvalidArgumentException("Provider '$provider' not found"); throw new InvalidArgumentException("Provider '$target' not found");
} }
return $providers[$provider]; return $providers[$target];
} }
/** /**
* Confirm which providers are available * Confirm which providers are available
* *
* @param SourceSelector|null $sources collection of provider identifiers to confirm * @param array<string> $targets collection of provider identifiers to confirm
* *
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false] * @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false]
*/ */
public function providerExtant(string $tenantId, string $userId, SourceSelector $sources): array { public function providerExtant(string $tenantId, string $userId, array $targets): array {
// determine which providers are available // determine which providers are available
$providersResolved = $this->providerList($tenantId, $userId, $sources); $providersResolved = $this->providerList($tenantId, $userId, $targets);
$providersAvailable = array_keys($providersResolved); $providersAvailable = array_keys($providersResolved);
$providersUnavailable = array_diff($sources->identifiers(), $providersAvailable); $providersUnavailable = array_diff($targets, $providersAvailable);
// construct response data // construct response data
$responseData = array_merge( $responseData = array_merge(
array_fill_keys($providersAvailable, true), array_fill_keys($providersAvailable, true),
@@ -111,18 +105,24 @@ class Manager {
* *
* @param string $tenantId tenant identifier * @param string $tenantId tenant identifier
* @param string $userId user identifier * @param string $userId user identifier
* @param SourceSelector|null $sources list of provider and service identifiers * @param ResourceIdentifiers|null $targets list of provider and service identifiers
* *
* @return array<string,<string,ServiceBaseInterface>> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]] * @return array<string,<string,ServiceBaseInterface>> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]]
*/ */
public function serviceList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { public function serviceList(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null): array {
// retrieve providers // retrieve providers
$providers = $this->providerList($tenantId, $userId, $sources); $providerFilter = $targets !== null ? $targets->providers() : null;
$providers = $this->providerList($tenantId, $userId, $providerFilter);
// retrieve services for each provider // retrieve services for each provider
$responseData = []; $responseData = [];
foreach ($providers as $provider) { foreach ($providers as $provider) {
$serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : []; if ($targets !== null) {
$services = $provider->serviceList($tenantId, $userId, $serviceFilter); $servicesSelected = $targets?->byProvider($provider->identifier()) ?: [];
$servicesFilter = $servicesSelected->services();
} else {
$servicesFilter = [];
}
$services = $provider->serviceList($tenantId, $userId, $servicesFilter);
$responseData[$provider->identifier()] = $services; $responseData[$provider->identifier()] = $services;
} }
return $responseData; return $responseData;
@@ -154,24 +154,23 @@ class Manager {
* *
* @param string $tenantId tenant identifier * @param string $tenantId tenant identifier
* @param string $userId user identifier * @param string $userId user identifier
* @param SourceSelector|null $sources collection of provider and service identifiers to confirm * @param ResourceIdentifiers $targets collection of provider and service identifiers to confirm
* *
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]] * @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]]
*/ */
public function serviceExtant(string $tenantId, string $userId, SourceSelector $sources): array { public function serviceExtant(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
// retrieve providers // retrieve available providers
$providers = $this->providerList($tenantId, $userId, $sources); $providersRequested = $targets->providers();
$providersRequested = $sources->identifiers(); $providers = $this->providerList($tenantId, $userId, $providersRequested);
$providersUnavailable = array_diff($providersRequested, array_keys($providers)); $providersUnavailable = array_diff($providersRequested, array_keys($providers));
// initialize response with unavailable providers // initialize response with unavailable providers
$responseData = array_fill_keys($providersUnavailable, false); $responseData = array_fill_keys($providersUnavailable, false);
// retrieve services for each available provider // retrieve services for each available provider
foreach ($providers as $provider) { foreach ($providers as $providerId => $provider) {
$serviceSelector = $sources[$provider->identifier()]; $servicesRequested = $targets->byProvider($providerId)->services();
$serviceAvailability = $provider->serviceExtant($tenantId, $userId, ...$serviceSelector->identifiers()); $responseData[$providerId] = $provider->serviceExtant($tenantId, $userId, ...$servicesRequested);
$responseData[$provider->identifier()] = $serviceAvailability;
} }
return $responseData; return $responseData;
} }
@@ -342,7 +341,7 @@ class Manager {
string|null $location = null, string|null $location = null,
string|null $secret = null string|null $secret = null
): \Generator { ): \Generator {
$providers = $this->providerList($tenantId, $userId, $providerId !== null ? new SourceSelector([$providerId => true]) : null); $providers = $this->providerList($tenantId, $userId, $providerId !== null ? [$providerId] : null);
foreach ($providers as $currentProviderId => $provider) { foreach ($providers as $currentProviderId => $provider) {
if ($provider instanceof ProviderServiceDiscoverInterface === false) { if ($provider instanceof ProviderServiceDiscoverInterface === false) {
@@ -459,26 +458,23 @@ class Manager {
* *
* @param string $tenantId Tenant identifier * @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context * @param string|null $userId User identifier for context
* @param SourceSelector|null $sources Provider/service sources * @param ResourceIdentifiers|null $targets Provider/service sources
* @param IFilter|null $filter Collection filter * @param IFilter|null $filter Collection filter
* @param ISort|null $sort Collection sort * @param ISort|null $sort Collection sort
* *
* @return array<string, array<string|int, array<string|int, ICollectionBase>>> Collections grouped by provider/service * @return array<string, array<string|int, array<string|int, ICollectionBase>>> Collections grouped by provider/service
*/ */
public function collectionList(string $tenantId, ?string $userId, ?SourceSelector $sources = null, ?IFilter $filter = null, ?ISort $sort = null): array { public function collectionList(string $tenantId, ?string $userId, ?ResourceIdentifiers $targets = null, ?IFilter $filter = null, ?ISort $sort = null): array {
// confirm that sources are provided // confirm that sources are provided
if ($sources === null) { if ($targets === null) {
$sources = new SourceSelector([]); $targets = new ResourceIdentifiers([]);
} }
// retrieve providers // retrieve providers
$providers = $this->providerList($tenantId, $userId, $sources); $aggregateServices = $this->serviceList($tenantId, $userId, $targets);
// retrieve services for each provider // retrieve services for each provider
$responseData = []; $responseData = [];
foreach ($providers as $provider) { // retrieve collections for each service
$serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : []; foreach ($aggregateServices as $services) {
/** @var ServiceBaseInterface[] $services */
$services = $provider->serviceList($tenantId, $userId, $serviceFilter);
// retrieve collections for each service
foreach ($services as $service) { foreach ($services as $service) {
if ($service->getEnabled() === false) { if ($service->getEnabled() === false) {
continue; continue;
@@ -499,67 +495,18 @@ class Manager {
$collectionSort->condition($attribute, $direction); $collectionSort->condition($attribute, $direction);
} }
} }
$collections = $service->collectionList('', $collectionFilter, $collectionSort);
if ($collections !== []) {
$responseData[$provider->identifier()][$service->identifier()] = $collections;
}
}
}
return $responseData;
}
/** $collectionIdentifiers = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
* Check if collections exist if ($collectionIdentifiers !== []) {
* foreach ($collectionIdentifiers as $collectionIdentifier) {
* @since 2025.05.01 $collections = array_merge($collections ?? [], $service->collectionList($collectionIdentifier, $collectionFilter, $collectionSort));
* }
* @param string $tenantId Tenant identifier } else {
* @param string|null $userId User identifier for context $collections = $service->collectionList('', $collectionFilter, $collectionSort);
* @param SourceSelector $sources Collection sources with identifiers
*
* @return array<string, array<string|int, array<string|int, bool>>> Existence map grouped by provider/service
*/
public function collectionExtant(string $tenantId, ?string $userId, SourceSelector $sources): array {
// retrieve available 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);
// check services and collections for each available provider
foreach ($providers as $provider) {
// extract services for this provider
$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->identifier()] = array_fill_keys($servicesUnavailable, false);
}
// confirm collections for each available service
foreach ($servicesAvailable as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
$responseData[$provider->identifier()][$service->identifier()] = false;
continue;
} }
// extract collections requested for this service if ($collections !== []) {
$collectionSelector = $serviceSelector[$service->identifier()]; $responseData[$service->provider()][$service->identifier()] = $collections;
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionsRequested === []) {
continue;
} }
// check each requested collection
$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; return $responseData;
@@ -578,14 +525,62 @@ class Manager {
* *
* @return CollectionBaseInterface|null * @return CollectionBaseInterface|null
*/ */
public function collectionFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ?CollectionBaseInterface { public function collectionFetch(string $tenantId, ?string $userId, CollectionIdentifier $target): ?CollectionBaseInterface {
// retrieve service // retrieve service
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); $service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
if ($service === null || $service->getEnabled() === false) {
return null;
}
// retrieve collection // retrieve collection
return $service->collectionFetch($collectionId); return $service->collectionFetch($target->collection());
}
/**
* Check if collections exist
*
* @since 2025.05.01
*
* @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context
* @param ResourceIdentifiers $targets Collection sources with identifiers
*
* @return array<string, array<string|int, array<string|int, bool>>> Existence map grouped by provider/service
*/
public function collectionExtant(string $tenantId, ?string $userId, ResourceIdentifiers $targets): array {
// retrieve available services grouped by provider
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
// initialize response with unavailable providers marked as false
$providersRequested = $targets->providers();
$providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
$responseData = array_fill_keys($providersUnavailable, false);
// check services and collections for each available provider
foreach ($aggregateServices as $providerId => $services) {
// mark unavailable services as false
$servicesRequested = $targets->byProvider($providerId)->services();
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
if ($servicesUnavailable !== []) {
$responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
}
// confirm collections for each available service
foreach ($services as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
$responseData[$providerId][$service->identifier()] = false;
continue;
}
// extract collections requested for this service
$collectionsRequested = $targets->byProvider($providerId)->byService($service->identifier())->collections();
if ($collectionsRequested === []) {
continue;
}
// check each requested collection
$collectionsAvailable = $service->collectionExtant(...$collectionsRequested);
$collectionsUnavailable = array_diff($collectionsRequested, array_keys($collectionsAvailable));
$responseData[$providerId][$service->identifier()] = array_merge(
$collectionsAvailable,
array_fill_keys($collectionsUnavailable, false)
);
}
}
return $responseData;
} }
/** /**
@@ -730,31 +725,31 @@ class Manager {
* *
* @param string $tenantId Tenant identifier * @param string $tenantId Tenant identifier
* @param string $userId User identifier * @param string $userId User identifier
* @param SourceSelector $sources Message sources with collection identifiers * @param ResourceIdentifiers|null $targets Message sources with collection identifiers
* @param array|null $filter Message filter * @param array|null $filter Message filter
* @param array|null $sort Message sort * @param array|null $sort Message sort
* @param array|null $range Message range/pagination * @param array|null $range Message range/pagination
* *
* @return array<string, array<string|int, array<string|int, array<string|int, IMessageBase>>>> Messages grouped by provider/service/collection * @return array<string, array<string|int, array<string|int, array<string|int, IMessageBase>>>> Messages grouped by provider/service/collection
*/ */
public function entityListBulk(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): array { public function entityListBulk(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): array {
// retrieve providers // confirm that sources are provided
$providers = $this->providerList($tenantId, $userId, $sources); if ($targets === null) {
$targets = new ResourceIdentifiers([]);
}
// retrieve services for each provider // retrieve services for each provider
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
// retrieve entities for each service
$responseData = []; $responseData = [];
foreach ($providers as $provider) { foreach ($aggregateServices as $services) {
// retrieve services for each provider
$serviceSelector = $sources[$provider->identifier()];
$servicesSelected = $provider->serviceList($tenantId,$userId, $serviceSelector->identifiers());
/** @var ServiceBaseInterface $service */ /** @var ServiceBaseInterface $service */
foreach ($servicesSelected as $service) { foreach ($services as $service) {
// omit disabled services // omit disabled services
if ($service->getEnabled() === false) { if ($service->getEnabled() === false) {
continue; continue;
} }
// retrieve collections for each service // retrieve collections for each service
$collectionSelector = $serviceSelector[$service->identifier()]; $collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionSelected === []) { if ($collectionSelected === []) {
$collections = $service->collectionList(''); $collections = $service->collectionList('');
$collectionSelected = array_map( $collectionSelected = array_map(
@@ -806,7 +801,7 @@ class Manager {
if ($entities === []) { if ($entities === []) {
continue; continue;
} }
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = $entities; $responseData[$service->provider()][$service->identifier()][$collectionId] = $entities;
} }
} }
} }
@@ -821,29 +816,29 @@ class Manager {
* *
* @param string $tenantId Tenant identifier * @param string $tenantId Tenant identifier
* @param string $userId User identifier * @param string $userId User identifier
* @param SourceSelector $sources Message sources with collection identifiers * @param ResourceIdentifiers|null $targets Message sources with collection identifiers
* @param array|null $filter Message filter * @param array|null $filter Message filter
* @param array|null $sort Message sort * @param array|null $sort Message sort
* @param array|null $range Message range/pagination * @param array|null $range Message range/pagination
* *
* @return \Generator<EntityBaseInterface> Yields each entity as it is retrieved * @return \Generator<EntityBaseInterface> Yields each entity as it is retrieved
*/ */
public function entityListStream(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator { public function entityListStream(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
// retrieve providers // confirm that sources are provided
$providers = $this->providerList($tenantId, $userId, $sources); if ($targets === null) {
$targets = new ResourceIdentifiers([]);
}
// retrieve services for each provider // retrieve services for each provider
foreach ($providers as $provider) { $aggregateServices = $this->serviceList($tenantId, $userId, $targets);
$serviceSelector = $sources[$provider->identifier()]; foreach ($aggregateServices as $services) {
$servicesSelected = $provider->serviceList($tenantId, $userId, $serviceSelector->identifiers());
/** @var ServiceBaseInterface $service */ /** @var ServiceBaseInterface $service */
foreach ($servicesSelected as $service) { foreach ($services as $service) {
// omit disabled services // omit disabled services
if ($service->getEnabled() === false) { if ($service->getEnabled() === false) {
continue; continue;
} }
// retrieve collections for each service // retrieve collections for each service
$collectionSelector = $serviceSelector[$service->identifier()]; $collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionSelected === []) { if ($collectionSelected === []) {
$collections = $service->collectionList(''); $collections = $service->collectionList('');
$collectionSelected = array_map( $collectionSelected = array_map(
@@ -971,43 +966,36 @@ class Manager {
* *
* @param string $tenantId Tenant identifier * @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context * @param string|null $userId User identifier for context
* @param SourceSelector $sources Message sources with identifiers * @param ResourceIdentifiers $targets Message sources with identifiers
* *
* @return array<string, array<string|int, array<string|int, array<string|int, bool>>>> Existence map grouped by provider/service/collection * @return array<string, array<string|int, array<string|int, array<string|int, bool>>>> Existence map grouped by provider/service/collection
*/ */
public function entityExtant(string $tenantId, string $userId, SourceSelector $sources): array { public function entityExtant(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
// confirm that sources are provided // retrieve available services grouped by provider
if ($sources === null) { $aggregateServices = $this->serviceList($tenantId, $userId, $targets);
$sources = new SourceSelector([]); // initialize response with unavailable providers marked as false
} $providersRequested = $targets->providers();
// retrieve available providers $providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
$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); $responseData = array_fill_keys($providersUnavailable, false);
// check services, collections, and entities for each available provider // check services, collections, and entities for each available provider
foreach ($providers as $provider) { foreach ($aggregateServices as $providerId => $services) {
// extract services requested for this provider
$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 // mark unavailable services as false
$servicesRequested = $targets->byProvider($providerId)->services();
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
if ($servicesUnavailable !== []) { if ($servicesUnavailable !== []) {
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false); $responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
} }
// check collections and entities for each available service // check collections and entities for each available service
foreach ($servicesAvailable as $service) { foreach ($services as $service) {
// omit disabled services // omit disabled services
if ($service->getEnabled() === false) { if ($service->getEnabled() === false) {
$responseData[$provider->identifier()][$service->identifier()] = false; $responseData[$providerId][$service->identifier()] = false;
continue; continue;
} }
// extract collections requested for this service // extract collections requested for this service
$collectionSelector = $serviceSelector[$service->identifier()]; $serviceTargets = $targets->byProvider($providerId)->byService($service->identifier());
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; $collectionsRequested = $serviceTargets->collections();
if ($collectionsRequested === []) { if ($collectionsRequested === []) {
continue; continue;
} }
@@ -1017,19 +1005,18 @@ class Manager {
$collectionExists = $service->collectionExtant((string)$collectionId); $collectionExists = $service->collectionExtant((string)$collectionId);
if (!$collectionExists) { if (!$collectionExists) {
// collection doesn't exist, mark as false // collection doesn't exist, mark as false
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = false; $responseData[$providerId][$service->identifier()][$collectionId] = false;
continue; continue;
} }
// extract entity identifiers from collection selector // extract entity identifiers requested for this collection
$entitySelector = $collectionSelector[$collectionId]; $entitiesRequested = $serviceTargets->byCollection($collectionId)->entities();
// handle both array of entity IDs and boolean true (meaning check if collection exists) if ($entitiesRequested === []) {
if ($entitySelector instanceof EntitySelector) {
// check specific entities within the collection
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = $service->entityExtant($collectionId, ...$entitySelector->identifiers());
} elseif ($entitySelector === true) {
// just checking if collection exists (already confirmed above) // just checking if collection exists (already confirmed above)
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = true; $responseData[$providerId][$service->identifier()][$collectionId] = true;
continue;
} }
// check specific entities within the collection
$responseData[$providerId][$service->identifier()][$collectionId] = $service->entityExtant($collectionId, ...$entitiesRequested);
} }
} }
} }
@@ -1043,50 +1030,45 @@ class Manager {
* *
* @param string $tenantId Tenant identifier * @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context * @param string|null $userId User identifier for context
* @param SourceSelector $sources Message sources with signatures * @param ResourceIdentifiers $targets Message sources with signatures
* *
* @return array<string, array<string|int, array<string|int, array>>> Delta grouped by provider/service/collection * @return array<string, array<string|int, array<string|int, array>>> Delta grouped by provider/service/collection
*/ */
public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { public function entityDelta(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
// confirm that sources are provided // retrieve available services grouped by provider
if ($sources === null) { $aggregateServices = $this->serviceList($tenantId, $userId, $targets);
$sources = new SourceSelector([]); // initialize response with unavailable providers marked as false
} $providersRequested = $targets->providers();
// retrieve providers $providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
$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); $responseData = array_fill_keys($providersUnavailable, false);
// iterate through available providers // iterate through available providers
foreach ($providers as $provider) { foreach ($aggregateServices as $providerId => $services) {
// extract services requested for this provider // mark unavailable services as false
$serviceSelector = $sources[$provider->identifier()]; $servicesRequested = $targets->byProvider($providerId)->services();
$servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : [];
/** @var ServiceBaseInterface[] $services */
$services = $provider->serviceList($tenantId, $userId, $servicesRequested);
$servicesUnavailable = array_diff($servicesRequested, array_keys($services)); $servicesUnavailable = array_diff($servicesRequested, array_keys($services));
if ($servicesUnavailable !== []) { if ($servicesUnavailable !== []) {
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false); $responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
} }
// iterate through available services // iterate through available services
foreach ($services as $service) { foreach ($services as $service) {
// omit disabled services // omit disabled services
if ($service->getEnabled() === false) { if ($service->getEnabled() === false) {
$responseData[$provider->identifier()][$service->identifier()] = false; $responseData[$providerId][$service->identifier()] = false;
continue; continue;
} }
// extract collections requested for this service // extract collections requested for this service
$collectionSelector = $serviceSelector[$service->identifier()]; $serviceTargets = $targets->byProvider($providerId)->byService($service->identifier());
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; $collectionsRequested = $serviceTargets->collections();
if ($collectionsRequested === []) { if ($collectionsRequested === []) {
$responseData[$provider->identifier()][$service->identifier()] = false; $responseData[$providerId][$service->identifier()] = false;
continue; continue;
} }
// check delta for each requested collection // check delta for each requested collection
foreach ($collectionsRequested as $collection) { foreach ($collectionsRequested as $collection) {
$entitySelector = $collectionSelector[$collection] ?? null; // signature for the collection is carried in the entity slot of the identifier
$responseData[$provider->identifier()][$service->identifier()][$collection] = $service->entityDelta($collection, $entitySelector); $signature = $serviceTargets->byCollection($collection)->entities()[0] ?? '';
$responseData[$providerId][$service->identifier()][$collection] = $service->entityDelta($collection, $signature);
} }
} }
} }