Merge pull request 'chore: code cleanup' (#6) from chore/code-cleanup into main
Some checks failed
Renovate / renovate (push) Failing after 1m20s

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2026-02-20 05:06:11 +00:00
8 changed files with 211 additions and 331 deletions

View File

@@ -14,8 +14,6 @@ use KTXC\Http\Response\JsonResponse;
use KTXC\SessionIdentity; use KTXC\SessionIdentity;
use KTXC\SessionTenant; use KTXC\SessionTenant;
use KTXF\Controller\ControllerAbstract; use KTXF\Controller\ControllerAbstract;
use KTXF\Mail\Entity\Message;
use KTXF\Mail\Queue\SendOptions;
use KTXF\Resource\Selector\SourceSelector; use KTXF\Resource\Selector\SourceSelector;
use KTXF\Routing\Attributes\AuthenticatedRoute; use KTXF\Routing\Attributes\AuthenticatedRoute;
use KTXM\MailManager\Manager; use KTXM\MailManager\Manager;
@@ -30,7 +28,6 @@ use Throwable;
*/ */
class DefaultController extends ControllerAbstract { class DefaultController extends ControllerAbstract {
// Error message constants
private const ERR_MISSING_PROVIDER = 'Missing parameter: provider'; private const ERR_MISSING_PROVIDER = 'Missing parameter: provider';
private const ERR_MISSING_IDENTIFIER = 'Missing parameter: identifier'; private const ERR_MISSING_IDENTIFIER = 'Missing parameter: identifier';
private const ERR_MISSING_SERVICE = 'Missing parameter: service'; private const ERR_MISSING_SERVICE = 'Missing parameter: service';
@@ -38,7 +35,7 @@ class DefaultController extends ControllerAbstract {
private const ERR_MISSING_DATA = 'Missing parameter: data'; private const ERR_MISSING_DATA = 'Missing parameter: data';
private const ERR_MISSING_SOURCES = 'Missing parameter: sources'; private const ERR_MISSING_SOURCES = 'Missing parameter: sources';
private const ERR_MISSING_IDENTIFIERS = 'Missing parameter: identifiers'; private const ERR_MISSING_IDENTIFIERS = 'Missing parameter: identifiers';
private const ERR_MISSING_MESSAGE = 'Missing parameter: message'; 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';
private const ERR_INVALID_IDENTIFIER = 'Invalid parameter: identifier must be a string'; private const ERR_INVALID_IDENTIFIER = 'Invalid parameter: identifier must be a string';
@@ -46,7 +43,6 @@ 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_IDENTIFIERS = 'Invalid parameter: identifiers 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'; private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
private const ERR_INVALID_MESSAGE = 'Invalid parameter: message must be an array';
public function __construct( public function __construct(
private readonly SessionTenant $tenantIdentity, private readonly SessionTenant $tenantIdentity,
@@ -59,23 +55,21 @@ class DefaultController extends ControllerAbstract {
* Main API endpoint for mail operations * Main API endpoint for mail operations
* *
* Single operation: * Single operation:
* { "version": 1, "transaction": "tx-1", "operation": "message.send", "data": {...} } * {
* * "version": 1,
* Batch operations: * "transaction": "tx-1",
* { "version": 1, "transaction": "tx-1", "operations": [ * "operation": "entity.create",
* {"id": "op1", "operation": "message.send", "data": {...}}, * "data": {...}
* {"id": "op2", "operation": "message.destroy", "data": {"collection": "#op1.draftId"}} * }
* ]}
* *
* @return JsonResponse * @return JsonResponse
*/ */
#[AuthenticatedRoute('/v1', name: 'mail.manager.v1', methods: ['POST'])] #[AuthenticatedRoute('/v1', name: 'mail.manager.v1', methods: ['POST'])]
public function index( public function index(
int $version, int $version,
string $transaction, string $transaction,
string|null $operation = null, string|null $operation = null,
array|null $data = null, array|null $data = null,
array|null $operations = null,
string|null $user = null string|null $user = null
): JsonResponse { ): JsonResponse {
@@ -84,7 +78,7 @@ class DefaultController extends ControllerAbstract {
$userId = $this->userIdentity->identifier(); $userId = $this->userIdentity->identifier();
try { try {
// Single operation mode
if ($operation !== null) { if ($operation !== null) {
$result = $this->processOperation($tenantId, $userId, $operation, $data ?? [], []); $result = $this->processOperation($tenantId, $userId, $operation, $data ?? [], []);
return new JsonResponse([ return new JsonResponse([
@@ -96,21 +90,10 @@ class DefaultController extends ControllerAbstract {
], JsonResponse::HTTP_OK); ], JsonResponse::HTTP_OK);
} }
// Batch operations mode throw new InvalidArgumentException('Operation must be provided');
if ($operations !== null && is_array($operations)) {
$results = $this->processBatch($tenantId, $userId, $operations);
return new JsonResponse([
'version' => $version,
'transaction' => $transaction,
'status' => 'success',
'operations' => $results
], JsonResponse::HTTP_OK);
}
throw new InvalidArgumentException('Either operation or operations must be provided');
} catch (Throwable $t) { } catch (Throwable $t) {
$this->logger->error('Error processing mail manager request', ['exception' => $t]); $this->logger->error('Error processing request', ['exception' => $t]);
return new JsonResponse([ return new JsonResponse([
'version' => $version, 'version' => $version,
'transaction' => $transaction, 'transaction' => $transaction,
@@ -124,105 +107,10 @@ class DefaultController extends ControllerAbstract {
} }
} }
/**
* Process batch operations with result references
*/
private function processBatch(string $tenantId, string $userId, array $operations): array {
$results = [];
$resultMap = []; // Store results by operation ID for references
foreach ($operations as $index => $op) {
$opId = $op['id'] ?? "op{$index}";
$operation = $op['operation'] ?? null;
$data = $op['data'] ?? [];
if ($operation === null) {
$results[] = [
'id' => $opId,
'status' => 'error',
'data' => ['message' => 'Missing operation name']
];
continue;
}
try {
// Resolve result references in data (e.g., "#op1.id")
$data = $this->resolveReferences($data, $resultMap);
$result = $this->processOperation($tenantId, $userId, $operation, $data, $resultMap);
$results[] = [
'id' => $opId,
'operation' => $operation,
'status' => 'success',
'data' => $result
];
// Store result for future references
$resultMap[$opId] = $result;
} catch (Throwable $t) {
$this->logger->warning('Batch operation failed', [
'operation' => $operation,
'opId' => $opId,
'error' => $t->getMessage()
]);
$results[] = [
'id' => $opId,
'operation' => $operation,
'status' => 'error',
'data' => [
'code' => $t->getCode(),
'message' => $t->getMessage()
]
];
}
}
return $results;
}
/**
* Resolve result references in operation data
*
* Transforms "#op1.id" into the actual value from previous operation results
*/
private function resolveReferences(mixed $data, array $resultMap): mixed {
if (is_string($data) && str_starts_with($data, '#')) {
// Parse reference like "#op1.id" or "#op1.collection.id"
$parts = explode('.', substr($data, 1));
$opId = array_shift($parts);
if (!isset($resultMap[$opId])) {
throw new InvalidArgumentException("Reference to undefined operation: #{$opId}");
}
$value = $resultMap[$opId];
foreach ($parts as $key) {
if (is_array($value) && isset($value[$key])) {
$value = $value[$key];
} elseif (is_object($value) && isset($value->$key)) {
$value = $value->$key;
} else {
throw new InvalidArgumentException("Invalid reference path: {$data}");
}
}
return $value;
}
if (is_array($data)) {
return array_map(fn($item) => $this->resolveReferences($item, $resultMap), $data);
}
return $data;
}
/** /**
* Process a single operation * Process a single operation
*/ */
private function processOperation(string $tenantId, string $userId, string $operation, array $data, array $resultMap): mixed { private function processOperation(string $tenantId, string $userId, string $operation, array $data): mixed {
return match ($operation) { return match ($operation) {
// Provider operations // Provider operations
'provider.list' => $this->providerList($tenantId, $userId, $data), 'provider.list' => $this->providerList($tenantId, $userId, $data),
@@ -261,7 +149,7 @@ class DefaultController extends ControllerAbstract {
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation), 'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
'entity.transmit' => $this->entityTransmit($tenantId, $userId, $data), 'entity.transmit' => $this->entityTransmit($tenantId, $userId, $data),
default => throw new InvalidArgumentException('Unknown operation: ' . $operation) default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation)
}; };
} }
@@ -279,6 +167,18 @@ class DefaultController extends ControllerAbstract {
} }
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->mailManager->providerFetch($tenantId, $userId, $data['identifier']);
}
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['sources'])) {
@@ -294,18 +194,6 @@ class DefaultController extends ControllerAbstract {
} }
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->mailManager->providerFetch($tenantId, $userId, $data['identifier']);
}
// ==================== Service Operations ===================== // ==================== Service Operations =====================
private function serviceList(string $tenantId, string $userId, array $data): mixed { private function serviceList(string $tenantId, string $userId, array $data): mixed {
@@ -320,20 +208,6 @@ class DefaultController extends ControllerAbstract {
} }
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->mailManager->serviceExtant($tenantId, $userId, $sources);
}
private function serviceFetch(string $tenantId, string $userId, array $data): mixed { private function serviceFetch(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['provider'])) { if (!isset($data['provider'])) {
@@ -352,41 +226,18 @@ class DefaultController extends ControllerAbstract {
return $this->mailManager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']); return $this->mailManager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']);
} }
private function serviceDiscover(string $tenantId, string $userId, array $data): mixed { private function serviceExtant(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['identity']) || empty($data['identity']) || !is_string($data['identity'])) {
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
}
$provider = $data['provider'] ?? null;
$identity = $data['identity'];
$location = $data['location'] ?? null;
$secret = $data['secret'] ?? null;
return $this->mailManager->serviceDiscover($tenantId, $userId, $provider, $identity, $location, $secret); if (!isset($data['sources'])) {
} throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
}
private function serviceTest(string $tenantId, string $userId, array $data): mixed { if (!is_array($data['sources'])) {
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
if (!isset($data['provider'])) { }
throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); $sources = new SourceSelector();
} $sources->jsonDeserialize($data['sources']);
if (!is_string($data['provider'])) {
throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER); return $this->mailManager->serviceExtant($tenantId, $userId, $sources);
}
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->mailManager->serviceTest(
$tenantId,
$userId,
$data['provider'],
$data['identifier'] ?? null,
$data['location'] ?? null,
$data['identity'] ?? null,
);
} }
private function serviceCreate(string $tenantId, string $userId, array $data): mixed { private function serviceCreate(string $tenantId, string $userId, array $data): mixed {
@@ -462,6 +313,43 @@ class DefaultController extends ControllerAbstract {
); );
} }
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->mailManager->serviceTest(
$tenantId,
$userId,
$data['provider'],
$data['identifier'] ?? null,
$data['location'] ?? null,
$data['identity'] ?? null,
);
}
private function serviceDiscover(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['identity']) || empty($data['identity']) || !is_string($data['identity'])) {
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
}
$provider = $data['provider'] ?? null;
$identity = $data['identity'];
$location = $data['location'] ?? null;
$secret = $data['secret'] ?? null;
return $this->mailManager->serviceDiscover($tenantId, $userId, $provider, $identity, $location, $secret);
}
// ==================== Collection Operations ==================== // ==================== Collection Operations ====================
private function collectionList(string $tenantId, string $userId, array $data): mixed { private function collectionList(string $tenantId, string $userId, array $data): mixed {
@@ -640,34 +528,6 @@ class DefaultController extends ControllerAbstract {
} }
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->mailManager->entityDelta($tenantId, $userId, $sources);
}
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->mailManager->entityExtant($tenantId, $userId, $sources);
}
private function entityFetch(string $tenantId, string $userId, array $data): mixed { private function entityFetch(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['provider'])) { if (!isset($data['provider'])) {
throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER);
@@ -704,6 +564,34 @@ class DefaultController extends ControllerAbstract {
); );
} }
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->mailManager->entityExtant($tenantId, $userId, $sources);
}
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->mailManager->entityDelta($tenantId, $userId, $sources);
}
private function entityTransmit(string $tenantId, string $userId, array $data): mixed { private function entityTransmit(string $tenantId, string $userId, array $data): mixed {
if (!isset($data['provider'])) { if (!isset($data['provider'])) {
throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER); throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER);

View File

@@ -38,8 +38,7 @@ use Psr\Log\LoggerInterface;
/** /**
* Mail Manager * Mail Manager
* *
* Provides unified mail sending across multiple providers with context-aware * Provides unified mail sending across multiple providers
* service discovery and queued delivery support.
*/ */
class Manager { class Manager {
@@ -63,6 +62,25 @@ class Manager {
return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $filter); return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $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];
}
/** /**
* Confirm which providers are available * Confirm which providers are available
* *
@@ -83,25 +101,6 @@ class Manager {
return $responseData; return $responseData;
} }
/**
* 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];
}
/** /**
* Retrieve available services for specific user * Retrieve available services for specific user
* *
@@ -124,6 +123,27 @@ class Manager {
return $responseData; 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 * Confirm which services are available
* *
@@ -151,27 +171,6 @@ class Manager {
return $responseData; 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;
}
/** /**
* Find a service that handles a specific email address * Find a service that handles a specific email address
* *
@@ -764,54 +763,26 @@ class Manager {
return $responseData; return $responseData;
} }
/** /**
* Get message delta/changes * Fetch specific messages
* *
* @since 2025.05.01 * @since 2025.05.01
* *
* @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 string $providerId Provider identifier
* @param string|int $serviceId Service identifier
* @param string|int $collectionId Collection identifier
* @param array<string|int> $identifiers Message identifiers
* *
* @return array<string, array<string|int, array<string|int, array>>> Delta grouped by provider/service/collection * @return array<string|int, IMessageBase> Messages indexed by ID
*/ */
public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { public function entityFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array {
// confirm that sources are provided $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
if ($sources === null) {
$sources = new SourceSelector([]); // retrieve collection
} return $service->entityFetch($collectionId, ...$identifiers);
// 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;
} }
/** /**
@@ -889,24 +860,52 @@ class Manager {
} }
/** /**
* Fetch specific messages * Get message delta/changes
* *
* @since 2025.05.01 * @since 2025.05.01
* *
* @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 string $providerId Provider identifier * @param SourceSelector $sources Message sources with signatures
* @param string|int $serviceId Service identifier
* @param string|int $collectionId Collection identifier
* @param array<string|int> $identifiers Message identifiers
* *
* @return array<string|int, IMessageBase> Messages indexed by ID * @return array<string, array<string|int, array<string|int, 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 { public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array {
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); // confirm that sources are provided
if ($sources === null) {
// retrieve collection $sources = new SourceSelector([]);
return $service->entityFetch($collectionId, ...$identifiers); }
// 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;
} }
/** /**

View File

@@ -1,13 +1,7 @@
/**
* Central export point for all Mail Manager models
*/
export { CollectionObject } from './collection'; export { CollectionObject } from './collection';
export { EntityObject } from './entity'; export { EntityObject } from './entity';
export { ProviderObject } from './provider'; export { ProviderObject } from './provider';
export { ServiceObject } from './service'; export { ServiceObject } from './service';
// Identity models
export { export {
Identity, Identity,
IdentityNone, IdentityNone,
@@ -16,8 +10,6 @@ export {
IdentityOAuth, IdentityOAuth,
IdentityCertificate IdentityCertificate
} from './identity'; } from './identity';
// Location models
export { export {
Location, Location,
LocationUri, LocationUri,

View File

@@ -5,21 +5,21 @@
import type { import type {
ServiceListRequest, ServiceListRequest,
ServiceListResponse, ServiceListResponse,
ServiceExtantRequest,
ServiceExtantResponse,
ServiceFetchRequest, ServiceFetchRequest,
ServiceFetchResponse, ServiceFetchResponse,
ServiceDiscoverRequest, ServiceExtantRequest,
ServiceDiscoverResponse, ServiceExtantResponse,
ServiceTestRequest,
ServiceTestResponse,
ServiceInterface,
ServiceCreateResponse, ServiceCreateResponse,
ServiceCreateRequest, ServiceCreateRequest,
ServiceUpdateResponse, ServiceUpdateResponse,
ServiceUpdateRequest, ServiceUpdateRequest,
ServiceDeleteResponse, ServiceDeleteResponse,
ServiceDeleteRequest, ServiceDeleteRequest,
ServiceDiscoverRequest,
ServiceDiscoverResponse,
ServiceTestRequest,
ServiceTestResponse,
ServiceInterface,
} from '../types/service'; } from '../types/service';
import { useIntegrationStore } from '@KTXC/stores/integrationStore'; import { useIntegrationStore } from '@KTXC/stores/integrationStore';
import { transceivePost } from './transceive'; import { transceivePost } from './transceive';

4
src/stores/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export { useCollectionsStore } from './collectionsStore';
export { useEntitiesStore } from './entitiesStore';
export { useProvidersStore } from './providersStore';
export { useServicesStore } from './servicesStore';

View File

@@ -1,7 +1,3 @@
/**
* Central export point for all Mail Manager types
*/
export type * from './collection'; export type * from './collection';
export type * from './common'; export type * from './common';
export type * from './entity'; export type * from './entity';

View File

@@ -11,8 +11,8 @@ export interface ProviderCapabilitiesInterface {
ServiceFetch?: boolean; ServiceFetch?: boolean;
ServiceExtant?: boolean; ServiceExtant?: boolean;
ServiceCreate?: boolean; ServiceCreate?: boolean;
ServiceModify?: boolean; ServiceUpdate?: boolean;
ServiceDestroy?: boolean; ServiceDelete?: boolean;
ServiceDiscover?: boolean; ServiceDiscover?: boolean;
ServiceTest?: boolean; ServiceTest?: boolean;
[key: string]: boolean | object | string[] | undefined; [key: string]: boolean | object | string[] | undefined;

View File

@@ -16,6 +16,7 @@ export interface ServiceCapabilitiesInterface {
CollectionCreate?: boolean; CollectionCreate?: boolean;
CollectionUpdate?: boolean; CollectionUpdate?: boolean;
CollectionDelete?: boolean; CollectionDelete?: boolean;
CollectionMove?: boolean;
// Message capabilities // Message capabilities
EntityList?: boolean; EntityList?: boolean;
EntityListFilter?: ServiceListFilterEntity; EntityListFilter?: ServiceListFilterEntity;