fix: clean up manager logic #21

Merged
Sebastian merged 1 commits from fix/cleanup-manager-logic into main 2026-05-15 23:07:29 +00:00
3 changed files with 222 additions and 148 deletions
Showing only changes of commit 72c2972777 - Show all commits

View File

@@ -316,8 +316,8 @@ class DefaultController extends ControllerAbstract {
$userId,
$data['provider'],
$data['identifier'],
$data['data'],
$data['delta'] ?? false,
$data['data']
);
}
@@ -515,7 +515,7 @@ class DefaultController extends ControllerAbstract {
$userId,
$data['provider'],
$data['service'],
$targetIdentifier->collection() ?? null,
$targetIdentifier ?? null,
$data['properties']
);
}
@@ -807,7 +807,7 @@ class DefaultController extends ControllerAbstract {
}
}
return $this->mailManager->entityDelete($tenantId, $userId, $sources);
return $this->mailManager->entityDelete($tenantId, $userId, ...$sources->all());
}
private function entityPatch(string $tenantId, string $userId, array $data): mixed {
@@ -860,7 +860,7 @@ class DefaultController extends ControllerAbstract {
}
}
return $this->mailManager->entityMove($tenantId, $userId, $target, $sources);
return $this->mailManager->entityMove($tenantId, $userId, $target, ...$sources->all());
}
private function entityTransmit(string $tenantId, string $userId, array $data): mixed {

View File

@@ -7,19 +7,14 @@ namespace KTXM\MailManager;
use InvalidArgumentException;
use KTXC\Resource\ProviderManager;
use KTXF\Mail\Collection\CollectionBaseInterface;
use KTXF\Mail\Collection\CollectionMutableInterface;
use KTXF\Mail\Collection\CollectionPropertiesMutableInterface;
use KTXF\Mail\Collection\ICollectionBase;
use KTXF\Mail\Entity\Address;
use KTXF\Mail\Entity\IMessageBase;
use KTXF\Mail\Entity\IMessageMutable;
use KTXF\Mail\Exception\SendException;
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
use KTXF\Mail\Provider\ProviderBaseInterface;
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
use KTXF\Mail\Provider\ProviderServiceMutateInterface;
use KTXF\Mail\Provider\ProviderServiceTestInterface;
use KTXF\Mail\Queue\SendOptions;
use KTXF\Mail\Service\IServiceSend;
use KTXF\Mail\Service\ServiceBaseInterface;
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
use KTXF\Mail\Service\ServiceConfigurableInterface;
@@ -28,11 +23,9 @@ use KTXF\Mail\Service\ServiceMutableInterface;
use KTXF\Resource\Filter\IFilter;
use KTXF\Resource\Identifier\CollectionIdentifier;
use KTXF\Resource\Identifier\EntityIdentifier;
use KTXF\Resource\Identifier\ResourceIdentifier;
use KTXF\Resource\Identifier\ResourceIdentifiers;
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Range\IRange;
use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeType;
use KTXF\Resource\Selector\CollectionSelector;
@@ -40,7 +33,6 @@ use KTXF\Resource\Selector\EntitySelector;
use KTXF\Resource\Selector\ServiceSelector;
use KTXF\Resource\Selector\SourceSelector;
use KTXF\Resource\Sort\ISort;
use KTXM\MailManager\Queue\MailQueueFile;
use Psr\Log\LoggerInterface;
/**
@@ -53,7 +45,6 @@ class Manager {
public function __construct(
private LoggerInterface $logger,
private ProviderManager $providerManager,
private MailQueueFile $queue,
) { }
/**
@@ -227,6 +218,9 @@ class Manager {
if ($provider instanceof ProviderServiceMutateInterface === false) {
throw new InvalidArgumentException("Provider '$providerId' does not support service creation");
}
if ($provider->capable(ProviderServiceMutateInterface::CAPABILITY_SERVICE_CREATE) === false) {
throw new InvalidArgumentException("Provider '$providerId' is not capable of creating services");
}
// Create a fresh service instance
$service = $provider->serviceFresh();
@@ -250,19 +244,22 @@ class Manager {
* @param string $userId User identifier for context
* @param string $providerId Provider identifier
* @param string|int $serviceId Service identifier
* @param bool $delta Whether the update is a delta (partial) update or a full replacement
* @param array $data Updated service configuration data
* @param bool $delta Whether the update is a delta (partial) update or a full replacement
*
* @return ServiceBaseInterface Updated service
*
* @throws InvalidArgumentException If provider doesn't support service modification or service not found
*/
public function serviceUpdate(string $tenantId, string $userId, string $providerId, string|int $serviceId, bool $delta = false, array $data): ServiceBaseInterface {
public function serviceUpdate(string $tenantId, string $userId, string $providerId, string|int $serviceId, array $data, bool $delta = false): ServiceBaseInterface {
// retrieve provider and service
$provider = $this->providerFetch($tenantId, $userId, $providerId);
if ($provider instanceof ProviderServiceMutateInterface === false) {
throw new InvalidArgumentException("Provider '$providerId' does not support service modification");
}
if ($provider->capable(ProviderServiceMutateInterface::CAPABILITY_SERVICE_MODIFY) === false) {
throw new InvalidArgumentException("Provider '$providerId' is not capable of modifying services");
}
// Fetch existing service
$service = $provider->serviceFetch($tenantId, $userId, $serviceId);
@@ -303,6 +300,9 @@ class Manager {
if ($provider instanceof ProviderServiceMutateInterface === false) {
throw new InvalidArgumentException("Provider '$providerId' does not support service deletion");
}
if ($provider->capable(ProviderServiceMutateInterface::CAPABILITY_SERVICE_DESTROY) === false) {
throw new InvalidArgumentException("Provider '$providerId' is not capable of deleting services");
}
// Fetch existing service
$service = $provider->serviceFetch($tenantId, $userId, $serviceId);
@@ -339,7 +339,7 @@ class Manager {
$providers = $this->providerList($tenantId, $userId, $providerId !== null ? new SourceSelector([$providerId => true]) : null);
foreach ($providers as $currentProviderId => $provider) {
if (!($provider instanceof ProviderServiceDiscoverInterface)) {
if ($provider instanceof ProviderServiceDiscoverInterface === false) {
continue;
}
@@ -399,7 +399,10 @@ class Manager {
if ($service === null) {
throw new InvalidArgumentException("Service not found: $providerId/$serviceId");
}
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service $providerId/$serviceId is disabled");
}
// test service
try {
return $provider->serviceTest($service);
} catch (\Throwable $e) {
@@ -471,6 +474,9 @@ class Manager {
$services = $provider->serviceList($tenantId, $userId, $serviceFilter);
// retrieve collections for each service
foreach ($services as $service) {
if ($service->getEnabled() === false) {
continue;
}
// construct filter for collections
$collectionFilter = null;
if ($filter !== null && $filter !== []) {
@@ -518,26 +524,29 @@ class Manager {
// 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
$collectionSelector = $serviceSelector[$service->identifier()];
$collectionsRequested = $collectionSelector->identifiers();
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionsRequested === []) {
continue;
}
// check each requested collection
$collectionsAvailable = $service->collectionExtant(...$collectionsRequested);
$collectionsUnavailable = array_diff($collectionsRequested, array_keys($collectionsAvailable));
@@ -566,7 +575,7 @@ class Manager {
public function collectionFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ?CollectionBaseInterface {
// retrieve service
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
if ($service === null) {
if ($service === null || $service->getEnabled() === false) {
return null;
}
// retrieve collection
@@ -588,21 +597,22 @@ class Manager {
public function collectionCreate(string $tenantId, string $userId, string $provider, string|int $service, CollectionIdentifier|null $target, CollectionPropertiesMutableInterface|array $properties, array $options = []): CollectionBaseInterface {
// retrieve service
$service = $this->serviceFetch($tenantId, $userId, $provider, $service);
// Check if service supports collection creation
if (!($service instanceof ServiceCollectionMutableInterface)) {
throw new InvalidArgumentException("Service does not support collection mutations");
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
}
if ($service instanceof ServiceCollectionMutableInterface === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support collection mutations");
}
if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_CREATE)) {
throw new InvalidArgumentException("Service is not capable of creating collections");
throw new InvalidArgumentException("Service '{$service->identifier()}' is not capable of creating collections");
}
// convert properties if necessary
if (is_array($properties)) {
$collection = $service->collectionFresh()->getProperties()->jsonDeserialize($properties);
} else {
$collection = $properties;
}
// Create collection
return $service->collectionCreate($target, $collection, $options);
}
@@ -621,21 +631,22 @@ class Manager {
public function collectionUpdate(string $tenantId, string $userId, CollectionIdentifier $target, CollectionPropertiesMutableInterface|array $properties): CollectionBaseInterface {
// retrieve service
$service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
// Check if service supports collection creation
if (!($service instanceof ServiceCollectionMutableInterface)) {
throw new InvalidArgumentException("Service does not support collection mutations");
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
}
if ($service instanceof ServiceCollectionMutableInterface === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support collection mutations");
}
if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_UPDATE)) {
throw new InvalidArgumentException("Service is not capable of updating collections");
throw new InvalidArgumentException("Service '{$service->identifier()}' is not capable of updating collections");
}
// convert properties if necessary
if (is_array($properties)) {
$mutation = $service->collectionFresh()->getProperties()->jsonDeserialize($properties);
} else {
$mutation = $properties;
}
// Update collection
return $service->collectionUpdate($target, $mutation);
}
@@ -655,17 +666,18 @@ class Manager {
public function collectionDelete(string $tenantId, ?string $userId, CollectionIdentifier $target, array $options = []): CollectionBaseInterface | bool {
// retrieve service
$service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
// Check if service supports collection deletion
if (!($service instanceof ServiceCollectionMutableInterface)) {
throw new InvalidArgumentException("Service does not support collection mutations");
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
}
if ($service instanceof ServiceCollectionMutableInterface === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support collection mutations");
}
if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_DELETE)) {
throw new InvalidArgumentException("Service is not capable of deleting collections");
throw new InvalidArgumentException("Service '{$service->identifier()}' is not capable of deleting collections");
}
// convert options
$force = $options['force'] ?? false;
// delete collection
return $service->collectionDelete($target, $force);
}
@@ -685,23 +697,24 @@ class Manager {
public function collectionMove(string $tenantId, ?string $userId, CollectionIdentifier $target, CollectionIdentifier $source): CollectionBaseInterface {
// validate that source and target are the same provider and service
if ($source->provider() !== $target->provider() || $source->service() !== $target->service()) {
throw new InvalidArgumentException("Source and target collections must belong to the same provider and service");
throw new InvalidArgumentException("Source '{$source->service()}' and target '{$target->service()}' collections must belong to the same provider and service");
}
// Validate that source and target are not the same
if ($source->collection() === $target->collection()) {
throw new InvalidArgumentException("Source and target collections are the same");
throw new InvalidArgumentException("Source '{$source->collection()}' and target '{$target->collection()}' collections are the same");
}
// retrieve service
$service = $this->serviceFetch($tenantId, $userId, $source->provider(), $source->service());
// Check if service supports collection move
if (!($service instanceof ServiceCollectionMutableInterface)) {
throw new InvalidArgumentException("Service does not support collection mutations");
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
}
if ($service instanceof ServiceCollectionMutableInterface === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support collection mutations");
}
if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_MOVE)) {
throw new InvalidArgumentException("Service is not capable of moving collections");
throw new InvalidArgumentException("Service '{$service->identifier()}' is not capable of moving collections");
}
// move collection
return $service->collectionMove($target, $source);
}
@@ -733,6 +746,10 @@ class Manager {
$servicesSelected = $provider->serviceList($tenantId,$userId, $serviceSelector->identifiers());
/** @var ServiceBaseInterface $service */
foreach ($servicesSelected as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
continue;
}
// retrieve collections for each service
$collectionSelector = $serviceSelector[$service->identifier()];
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
@@ -818,6 +835,10 @@ class Manager {
$servicesSelected = $provider->serviceList($tenantId, $userId, $serviceSelector->identifiers());
/** @var ServiceBaseInterface $service */
foreach ($servicesSelected as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
continue;
}
// retrieve collections for each service
$collectionSelector = $serviceSelector[$service->identifier()];
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
@@ -888,7 +909,9 @@ class Manager {
*/
public function entityFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array {
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
if ($service->getEnabled() === false) {
throw new InvalidArgumentException("Service '{$providerId}:{$serviceId}' not found or is disabled");
}
// retrieve collection
return $service->entityFetch($collectionId, ...$identifiers);
}
@@ -913,46 +936,44 @@ class Manager {
$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, collections, and entities for each available provider
foreach ($providers as $provider) {
// 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
if ($servicesUnavailable !== []) {
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false);
}
// check collections and entities 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
$collectionSelector = $serviceSelector[$service->identifier()];
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionsRequested === []) {
continue;
}
// check entities for each requested collection
foreach ($collectionsRequested as $collectionId) {
// first check if collection exists
$collectionExists = $service->collectionExtant((string)$collectionId);
if (!$collectionExists) {
// collection doesn't exist, mark as false
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = false;
continue;
}
// extract entity identifiers from collection selector
$entitySelector = $collectionSelector[$collectionId];
// handle both array of entity IDs and boolean true (meaning check if collection exists)
if ($entitySelector instanceof EntitySelector) {
// check specific entities within the collection
@@ -991,6 +1012,7 @@ class Manager {
$responseData = array_fill_keys($providersUnavailable, false);
// iterate through available providers
foreach ($providers as $provider) {
// extract services requested for this provider
$serviceSelector = $sources[$provider->identifier()];
$servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : [];
/** @var ServiceBaseInterface[] $services */
@@ -1001,12 +1023,19 @@ class Manager {
}
// iterate through available services
foreach ($services as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
$responseData[$provider->identifier()][$service->identifier()] = false;
continue;
}
// extract collections requested for this service
$collectionSelector = $serviceSelector[$service->identifier()];
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
if ($collectionsRequested === []) {
$responseData[$provider->identifier()][$service->identifier()] = false;
continue;
}
// check delta for each requested collection
foreach ($collectionsRequested as $collection) {
$entitySelector = $collectionSelector[$collection] ?? null;
$responseData[$provider->identifier()][$service->identifier()][$collection] = $service->entityDelta($collection, $entitySelector);
@@ -1017,61 +1046,72 @@ class Manager {
}
/**
* Deletes entities
* Patches entities
*
* @since 2026.04.01
*
* @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context
* @param ResourceIdentifiers $sources Source entities to delete
* @param MessagePropertiesMutableInterface|array $properties Properties to patch
* @param EntityIdentifier ...$targets Target entities to patch
*
* @return array<string, array{
* disposition: 'moved'|'deleted'|'error',
* destination: ?CollectionIdentifier,
* mutation: EntityIdentifier
* disposition: 'patched'|'error',
* error?: string
* }> Results keyed by source entity identifier
*/
public function entityDelete(string $tenantId, string $userId, ResourceIdentifiers $sources): array {
public function entityPatch(string $tenantId, string $userId, MessagePropertiesMutableInterface|array $properties, EntityIdentifier ...$targets): array {
$operationOutcome = [];
$targetIdentifiers = new ResourceIdentifiers();
foreach ($sources->providers() as $providerName) {
$providerSources = $sources->byProvider($providerName);
foreach ($providerSources->services() as $serviceName) {
$serviceSources = $providerSources->byService($serviceName);
foreach ($targets as $target) {
$targetIdentifiers->add($target);
}
// process targets grouped by provider
foreach ($targetIdentifiers->providers() as $providerId) {
// retrieve provider and validate
$provider = $this->providerFetch($tenantId, $userId, $providerId);
$providerTargets = $targetIdentifiers->byProvider($providerId);
// provider not found, mark all identifiers as failed and continue to next provider
if ($provider === null) {
foreach ($providerTargets->services() as $serviceId) {
$serviceTargets = $providerTargets->byService($serviceId);
foreach ($serviceTargets as $identifier) {
$operationOutcome[$identifier] = ['disposition' => 'error', 'error' => 'provider not found'];
}
}
continue;
}
// process targets grouped by service for this provider
foreach ($providerTargets->services() as $serviceId) {
// extract services requested for this provider
$serviceTargets = $providerTargets->byService($serviceId);
// retrieve and validate service
$service = null;
try {
$service = $this->serviceFetch($tenantId, $userId, $providerName, $serviceName);
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
} catch (\Throwable $e) {
// Service not found, mark all identifiers as failed
$error = "Service $serviceId not found";
}
if ($service === null) {
throw new InvalidArgumentException("Service not found: $providerName/$serviceName");
if ($service instanceof ServiceEntityMutableInterface === false && !isset($error)) {
$error = "Service $serviceId does not support entity mutation";
}
// Temporarily disabled check until all methods are properly implemented from ServiceEntityMutableInterface
if (!($service instanceof ServiceEntityMutableInterface)) {
foreach ($serviceSources as $identifier) {
$operationOutcome[(string)$identifier] = [
'success' => false,
'error' => 'serviceNotEntityMutable',
];
if (!isset($error) && !$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_PATCH)) {
$error = "Service $serviceId does not support entity patching";
}
// on error, mark all identifiers for this service as failed and continue to next service
if (isset($error)) {
foreach ($serviceTargets as $identifier) {
$operationOutcome[(string)$identifier] = ['disposition' => 'error','error' => $error];
}
continue;
}
if (!$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_DELETE)) {
foreach ($serviceSources as $identifier) {
$operationOutcome[(string)$identifier] = [
'success' => false,
'error' => 'serviceCannotDeleteEntities',
];
}
continue;
/** @var ServiceEntityMutableInterface $service */
if ($properties instanceof MessagePropertiesMutableInterface === false) {
$properties = $service->entityFresh()->getProperties()->jsonDeserialize($properties);
}
return $service->entityDelete(...$serviceSources->all());
$operationOutcome = array_merge($operationOutcome, $service->entityPatch($properties, ...$serviceTargets->all()));
}
}
@@ -1079,67 +1119,69 @@ class Manager {
}
/**
* Patches entities
* Deletes entities
*
* @since 2026.04.01
*
* @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context
* @param ResourceIdentifiers $sources Source entities to patch
* @param array $patchData Data to patch
* @param EntityIdentifier $targets Source entities to delete
*
* @return array<string, array{
* disposition: 'patched'|'error',
* error?: string
* disposition: 'moved'|'deleted'|'error',
* destination: ?CollectionIdentifier,
* mutation: EntityIdentifier
* }> Results keyed by source entity identifier
*/
public function entityPatch(string $tenantId, string $userId, ResourceIdentifiers $targets, array $patch): array {
public function entityDelete(string $tenantId, string $userId, EntityIdentifier ...$targets): array {
$operationOutcome = [];
$targetIdentifiers = new ResourceIdentifiers();
// Process targets grouped by provider and service to minimize redundant fetches and operations
foreach ($targets->providers() as $providerName) {
$providerTargets = $targets->byProvider($providerName);
$provider = $this->providerFetch($tenantId, $userId, $providerName);
foreach ($targets as $target) {
$targetIdentifiers->add($target);
}
// If provider is not found, mark all identifiers as failed and continue to next provider
// process targets grouped by provider
foreach ($targetIdentifiers->providers() as $providerId) {
// retrieve provider and validate
$provider = $this->providerFetch($tenantId, $userId, $providerId);
$providerTargets = $targetIdentifiers->byProvider($providerId);
// provider not found, mark all identifiers as failed and continue to next provider
if ($provider === null) {
foreach ($providerTargets->services() as $serviceName) {
$serviceTargets = $providerTargets->byService($serviceName);
foreach ($providerTargets->services() as $serviceId) {
$serviceTargets = $providerTargets->byService($serviceId);
foreach ($serviceTargets as $identifier) {
$operationOutcome[$identifier] = ['disposition' => 'error', 'error' => 'provider not found'];
}
}
continue;
}
foreach ($providerTargets->services() as $serviceName) {
$serviceTargets = $providerTargets->byService($serviceName);
$service = $this->serviceFetch($tenantId, $userId, $providerName, $serviceName);
// If service is not found, mark all identifiers as failed and continue to next service
if ($service === null) {
// process targets grouped by service for this provider
foreach ($providerTargets->services() as $serviceId) {
// extract services requested for this provider
$serviceTargets = $providerTargets->byService($serviceId);
// retrieve and validate service
$service = null;
try {
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
} catch (\Throwable $e) {
$error = "Service $serviceId not found";
}
if ($service instanceof ServiceEntityMutableInterface === false && !isset($error)) {
$error = "Service $serviceId does not support entity mutation";
}
if (!isset($error) && !$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_DELETE)) {
$error = "Service $serviceId does not support entity deletion";
}
// on error, mark all identifiers for this service as failed and continue to next service
if (isset($error)) {
foreach ($serviceTargets as $identifier) {
$operationOutcome[$identifier] = ['disposition' => 'error', 'error' => 'service not found'];
$operationOutcome[(string)$identifier] = ['disposition' => 'error', 'error' => $error];
}
continue;
}
// If service does not support entity mutation, mark all identifiers as failed and continue to next service
if (!($service instanceof ServiceEntityMutableInterface)) {
foreach ($serviceTargets as $identifier) {
$operationOutcome[$identifier] = ['disposition' => 'error', 'error' => 'entity mutations not supported'];
}
continue;
}
// If service does not support entity patching, mark all identifiers as failed and continue to next service
if (!$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_PATCH)) {
foreach ($serviceTargets as $identifier) {
$operationOutcome[$identifier] = ['disposition' => 'error', 'error' => 'entity patching not supported'];
}
continue;
}
$properties = $service->entityFresh()->getProperties()->jsonDeserialize($patch);
return $service->entityPatch(...$serviceTargets->all(), patch: $patch);
/** @var ServiceEntityMutableInterface $service */
$operationOutcome = array_merge($operationOutcome, $service->entityDelete(...$serviceTargets->all()));
}
}
@@ -1154,7 +1196,7 @@ class Manager {
* @param string $tenantId Tenant identifier
* @param string|null $userId User identifier for context
* @param CollectionIdentifier $target Target collection identifier
* @param ResourceIdentifiers $sources Source entities to move
* @param EntityIdentifier ...$sources Source entities to move
*
* @return array<string, array{
* disposition: 'moved'|'error',
@@ -1162,24 +1204,54 @@ class Manager {
* mutation: EntityIdentifier
* }> Results keyed by source entity identifier
*/
public function entityMove(string $tenantId, string $userId, CollectionIdentifier $target, ResourceIdentifiers $sources): array {
$targetService = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
// Check if service supports entity move
if ($targetService instanceof ServiceEntityMutableInterface === false) {
return [];
}
if (!$targetService->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_MOVE)) {
return [];
}
public function entityMove(string $tenantId, string $userId, CollectionIdentifier $target, EntityIdentifier ...$sources): array {
$operationOutcome = [];
$destinationSources = $sources->byProvider($targetService->provider())->byService((string)$targetService->identifier());
if (!$destinationSources->isEmpty()) {
$operationOutcome = $targetService->entityMove($target, ...$destinationSources->all());
// retrieve and validate service
$targetService = null;
try {
$targetService = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
} catch (\Throwable $e) {
// do nothing here, error will be handled in validation below
}
if ($targetService === null || $targetService->getEnabled() === false) {
$error = "Service {$target->service()} not found or is disabled";
}
/** @var ServiceEntityBaseInterface $targetService */
if ($targetService instanceof ServiceEntityMutableInterface === false && !isset($error)) {
$error = "Service {$targetService->identifier()} does not support entity mutation";
}
if (!isset($error) && !$targetService->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_MOVE)) {
$error = "Service {$targetService->identifier()} does not support entity moving";
}
// on error, mark all identifiers for this service as failed and continue to next service
if (isset($error)) {
foreach ($sources as $identifier) {
$operationOutcome[(string)$identifier] = ['disposition' => 'error','error' => $error];
}
return $operationOutcome;
}
// validate that sources and target are the same service and group sources by service for processing
$groupedSources = [];
foreach ($sources as $source) {
if ($source->provider() !== $target->provider() || $source->service() !== $target->service()) {
$operationOutcome[(string)$source] = [
'disposition' => 'error',
'error' => "Source '{$source}' and target '{$target}' collections must belong to the same provider and service"
];
continue;
}
$groupedSources[$source->provider()][$source->service()][] = $source;
}
if ($groupedSources === []) {
return $operationOutcome;
}
// perform operation for entities on the same service as the target
$operationOutcome = array_merge(
$operationOutcome,
$targetService->entityMove($target, ...$groupedSources[$target->provider()][$target->service()])
);
// TODO: Handle moving entities across different services/providers by fetching each entity and re-creating it in the target collection,
// then deleting the original if the move is successful. This will require additional logic to handle potential failures and ensure data integrity.
@@ -1187,4 +1259,5 @@ class Manager {
return $operationOutcome;
}
}

View File

@@ -203,6 +203,7 @@ function handleNextStep() {
async function handleDiscover() {
// Move to discovery status screen
isManualMode.value = false
currentStep.value = DISCOVERY_STEPS.DISCOVERY
// Extract provider IDs