Merge pull request 'chore: code cleanup' (#6) from chore/code-cleanup into main
Some checks failed
Renovate / renovate (push) Failing after 1m20s
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:
@@ -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,13 +55,12 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -75,7 +70,6 @@ class DefaultController extends ControllerAbstract {
|
|||||||
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'])) {
|
if (!isset($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
|
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']);
|
||||||
|
|
||||||
$provider = $data['provider'] ?? null;
|
return $this->mailManager->serviceExtant($tenantId, $userId, $sources);
|
||||||
$identity = $data['identity'];
|
|
||||||
$location = $data['location'] ?? null;
|
|
||||||
$secret = $data['secret'] ?? null;
|
|
||||||
|
|
||||||
return $this->mailManager->serviceDiscover($tenantId, $userId, $provider, $identity, $location, $secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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);
|
||||||
|
|||||||
183
lib/Manager.php
183
lib/Manager.php
@@ -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
|
||||||
*
|
*
|
||||||
@@ -766,52 +765,24 @@ class Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
4
src/stores/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { useCollectionsStore } from './collectionsStore';
|
||||||
|
export { useEntitiesStore } from './entitiesStore';
|
||||||
|
export { useProvidersStore } from './providersStore';
|
||||||
|
export { useServicesStore } from './servicesStore';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user