From 72c2972777ebc1a87932b8fe1cb0753046432693 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 15 May 2026 19:06:06 -0400 Subject: [PATCH] fix: clean up manager logic Signed-off-by: Sebastian Krupinski --- lib/Controllers/DefaultController.php | 8 +- lib/Manager.php | 361 ++++++++++++++++---------- src/components/AddAccountDialog.vue | 1 + 3 files changed, 222 insertions(+), 148 deletions(-) diff --git a/lib/Controllers/DefaultController.php b/lib/Controllers/DefaultController.php index cb30001..83638d2 100644 --- a/lib/Controllers/DefaultController.php +++ b/lib/Controllers/DefaultController.php @@ -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 { diff --git a/lib/Manager.php b/lib/Manager.php index 56a7897..0d611c7 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -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 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 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 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; } + } diff --git a/src/components/AddAccountDialog.vue b/src/components/AddAccountDialog.vue index 0f62246..de55080 100644 --- a/src/components/AddAccountDialog.vue +++ b/src/components/AddAccountDialog.vue @@ -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