From 318a8671c80ec8b9368b8139b14bea3e8b812e2b Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 18:15:29 +0100 Subject: [PATCH 1/8] Make Appwrite source report resilient to per-resource failures Probe reachability first, then report each resource group independently: an unsupported resource (404 on an older/partial source) is recorded and skipped instead of aborting the whole report. Invalid credentials (401) and missing scopes (403) are still surfaced. --- src/Migration/Sources/Appwrite.php | 61 ++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 4cf069ae..f6aca96b 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -280,18 +280,8 @@ public function report(array $resources = [], array $resourceIds = []): array $resources = $this->getSupportedResources(); } + // Reachability gate: unauthenticated, so a failure is a wrong/unreachable endpoint. try { - $this->reportAuth($resources, $report, $resourceIds); - $this->reportDatabases($resources, $report, $resourceIds); - $this->reportStorage($resources, $report, $resourceIds); - $this->reportFunctions($resources, $report, $resourceIds); - $this->reportMessaging($resources, $report, $resourceIds); - $this->reportSites($resources, $report, $resourceIds); - $this->reportIntegrations($resources, $report, $resourceIds); - $this->reportBackups($resources, $report, $resourceIds); - $this->reportProjects($resources, $report, $resourceIds); - $this->reportDomains($resources, $report, $resourceIds); - $report['version'] = $this->call( 'GET', '/health/version', @@ -301,13 +291,54 @@ public function report(array $resources = [], array $resourceIds = []): array ] )['version']; } catch (\Throwable $e) { - if ($e->getCode() === 403) { - throw new \Exception('Missing required scopes.', $e->getCode(), $e); - } else { - throw new \Exception($e->getMessage(), $e->getCode(), $e); + throw new \Exception('Unable to reach the migration source endpoint.', $e->getCode(), $e); + } + + // Report groups independently: 404/unsupported is recorded and skipped; 401 and 403 surface. + $groups = [ + Transfer::GROUP_AUTH => fn () => $this->reportAuth($resources, $report, $resourceIds), + Transfer::GROUP_DATABASES => fn () => $this->reportDatabases($resources, $report, $resourceIds), + Transfer::GROUP_STORAGE => fn () => $this->reportStorage($resources, $report, $resourceIds), + Transfer::GROUP_FUNCTIONS => fn () => $this->reportFunctions($resources, $report, $resourceIds), + Transfer::GROUP_MESSAGING => fn () => $this->reportMessaging($resources, $report, $resourceIds), + Transfer::GROUP_SITES => fn () => $this->reportSites($resources, $report, $resourceIds), + Transfer::GROUP_INTEGRATIONS => fn () => $this->reportIntegrations($resources, $report, $resourceIds), + Transfer::GROUP_BACKUPS => fn () => $this->reportBackups($resources, $report, $resourceIds), + Transfer::GROUP_PROJECTS => fn () => $this->reportProjects($resources, $report, $resourceIds), + Transfer::GROUP_DOMAINS => fn () => $this->reportDomains($resources, $report, $resourceIds), + ]; + + $missingScopes = []; + + foreach ($groups as $group => $reporter) { + try { + $reporter(); + } catch (\Throwable $e) { + $code = $e->getCode(); + + if ($code === Exception::CODE_UNAUTHORIZED) { + throw new \Exception('Invalid credentials for the migration source.', $code, $e); + } + + if ($code === Exception::CODE_FORBIDDEN) { + $missingScopes[] = $group; + continue; + } + + $this->addError(new Exception( + resourceName: $group, + resourceGroup: $group, + message: $e->getMessage(), + code: $code ?: Exception::CODE_INTERNAL, + previous: $e, + )); } } + if (!empty($missingScopes)) { + throw new \Exception('Missing required scopes for: ' . \implode(', ', $missingScopes) . '.', Exception::CODE_FORBIDDEN); + } + $this->previousReport = $report; return $report; From af39ae58be9ce959f2dd5bd8fc0396fa8235eb1e Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 18:35:23 +0100 Subject: [PATCH 2/8] Skip only unsupported (404) resources, fail fast on other errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review: only a 404 (resource unsupported by the source) is skippable. Missing scopes (403) — including those previously swallowed by inner catches in integrations/projects/domains — now surface as 'Missing required scopes'. Invalid key, rate limit, server, and connectivity errors fail fast instead of returning a partial report. Removes group-level addError recording that status counters dropped. --- src/Migration/Sources/Appwrite.php | 50 +++++++++++++++++------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index f6aca96b..d4caee74 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -294,7 +294,8 @@ public function report(array $resources = [], array $resourceIds = []): array throw new \Exception('Unable to reach the migration source endpoint.', $e->getCode(), $e); } - // Report groups independently: 404/unsupported is recorded and skipped; 401 and 403 surface. + // Report groups independently: an unsupported resource (404) is skipped; missing scopes + // (403) and any other failure (invalid key, rate limit, server, connectivity) are surfaced. $groups = [ Transfer::GROUP_AUTH => fn () => $this->reportAuth($resources, $report, $resourceIds), Transfer::GROUP_DATABASES => fn () => $this->reportDatabases($resources, $report, $resourceIds), @@ -314,24 +315,13 @@ public function report(array $resources = [], array $resourceIds = []): array try { $reporter(); } catch (\Throwable $e) { - $code = $e->getCode(); - - if ($code === Exception::CODE_UNAUTHORIZED) { - throw new \Exception('Invalid credentials for the migration source.', $code, $e); - } - - if ($code === Exception::CODE_FORBIDDEN) { + // Missing scope is user-fixable — collect every affected group and report together. + if ($e->getCode() === Exception::CODE_FORBIDDEN) { $missingScopes[] = $group; continue; } - $this->addError(new Exception( - resourceName: $group, - resourceGroup: $group, - message: $e->getMessage(), - code: $code ?: Exception::CODE_INTERNAL, - previous: $e, - )); + $this->rethrowUnlessUnsupported($e); } } @@ -344,6 +334,18 @@ public function report(array $resources = [], array $resourceIds = []): array return $report; } + /** + * Re-throw unless the source simply does not support the resource (404). Optional resources + * then degrade to an empty count, while real failures — missing scope, rate limit, server + * error, connectivity — propagate and fail the report instead of returning partial data. + */ + private function rethrowUnlessUnsupported(\Throwable $e): void + { + if ($e->getCode() !== Exception::CODE_NOT_FOUND) { + throw $e; + } + } + /** * @param array $resources * @param array $report @@ -1682,7 +1684,8 @@ private function reportDomains(array $resources, array &$report, array $resource if (\in_array(Resource::TYPE_RULE, $resources)) { try { $report[Resource::TYPE_RULE] = $this->proxy->listRules([Query::limit(1)])->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_RULE] = 0; } } @@ -1698,7 +1701,8 @@ private function reportProjects(array $resources, array &$report, array $resourc ); try { $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_PROJECT_VARIABLE] = 0; } } @@ -1722,7 +1726,8 @@ private function reportProjects(array $resources, array &$report, array $resourc try { // total:true returns the real count without fetching every row. $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = $this->project->listEmailTemplates([Query::limit(1)], total: true)->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = 0; } } @@ -2878,7 +2883,8 @@ private function reportIntegrations(array $resources, array &$report, array $res ); try { $report[Resource::TYPE_PLATFORM] = $this->project->listPlatforms($platformQueries)->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_PLATFORM] = 0; } } @@ -2891,7 +2897,8 @@ private function reportIntegrations(array $resources, array &$report, array $res ); try { $report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_API_KEY] = 0; } } @@ -2904,7 +2911,8 @@ private function reportIntegrations(array $resources, array &$report, array $res ); try { $report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total; - } catch (\Throwable) { + } catch (\Throwable $e) { + $this->rethrowUnlessUnsupported($e); $report[Resource::TYPE_WEBHOOK] = 0; } } From 5f8deb7180df72ca78843884b2486da1234ab0eb Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 19:26:05 +0100 Subject: [PATCH 3/8] Fail fast on report errors except aggregated missing scopes Per review: drop the 404 skip. A 404 is overloaded (item-level vs unsupported-service) and silent skips hid groups from getErrors(). report() now fails on any non-403 error, matching its original fail-fast behavior, while still aggregating 403s across groups and letting optional reporters' inner catches propagate 403 instead of zeroing it. --- src/Migration/Sources/Appwrite.php | 33 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index d4caee74..463ba63b 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -294,8 +294,8 @@ public function report(array $resources = [], array $resourceIds = []): array throw new \Exception('Unable to reach the migration source endpoint.', $e->getCode(), $e); } - // Report groups independently: an unsupported resource (404) is skipped; missing scopes - // (403) and any other failure (invalid key, rate limit, server, connectivity) are surfaced. + // Report groups independently. A missing scope (403) is collected and surfaced together; + // any other failure fails the report. $groups = [ Transfer::GROUP_AUTH => fn () => $this->reportAuth($resources, $report, $resourceIds), Transfer::GROUP_DATABASES => fn () => $this->reportDatabases($resources, $report, $resourceIds), @@ -316,12 +316,12 @@ public function report(array $resources = [], array $resourceIds = []): array $reporter(); } catch (\Throwable $e) { // Missing scope is user-fixable — collect every affected group and report together. - if ($e->getCode() === Exception::CODE_FORBIDDEN) { - $missingScopes[] = $group; - continue; + // Any other failure fails the report rather than returning partial counts. + if ($e->getCode() !== Exception::CODE_FORBIDDEN) { + throw $e; } - $this->rethrowUnlessUnsupported($e); + $missingScopes[] = $group; } } @@ -335,13 +335,12 @@ public function report(array $resources = [], array $resourceIds = []): array } /** - * Re-throw unless the source simply does not support the resource (404). Optional resources - * then degrade to an empty count, while real failures — missing scope, rate limit, server - * error, connectivity — propagate and fail the report instead of returning partial data. + * Surface a missing-scope (403) failure from an optional reporter so report() can aggregate it; + * other failures are left for the caller to swallow to a zero count. */ - private function rethrowUnlessUnsupported(\Throwable $e): void + private function rethrowIfForbidden(\Throwable $e): void { - if ($e->getCode() !== Exception::CODE_NOT_FOUND) { + if ($e->getCode() === Exception::CODE_FORBIDDEN) { throw $e; } } @@ -1685,7 +1684,7 @@ private function reportDomains(array $resources, array &$report, array $resource try { $report[Resource::TYPE_RULE] = $this->proxy->listRules([Query::limit(1)])->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_RULE] = 0; } } @@ -1702,7 +1701,7 @@ private function reportProjects(array $resources, array &$report, array $resourc try { $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_PROJECT_VARIABLE] = 0; } } @@ -1727,7 +1726,7 @@ private function reportProjects(array $resources, array &$report, array $resourc // total:true returns the real count without fetching every row. $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = $this->project->listEmailTemplates([Query::limit(1)], total: true)->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = 0; } } @@ -2884,7 +2883,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_PLATFORM] = $this->project->listPlatforms($platformQueries)->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_PLATFORM] = 0; } } @@ -2898,7 +2897,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_API_KEY] = 0; } } @@ -2912,7 +2911,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total; } catch (\Throwable $e) { - $this->rethrowUnlessUnsupported($e); + $this->rethrowIfForbidden($e); $report[Resource::TYPE_WEBHOOK] = 0; } } From a712360c211570fc463324f555005e1b5fe76a76 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 19:29:57 +0100 Subject: [PATCH 4/8] Trim report() comments --- src/Migration/Sources/Appwrite.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 463ba63b..d9e29fd4 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -294,8 +294,6 @@ public function report(array $resources = [], array $resourceIds = []): array throw new \Exception('Unable to reach the migration source endpoint.', $e->getCode(), $e); } - // Report groups independently. A missing scope (403) is collected and surfaced together; - // any other failure fails the report. $groups = [ Transfer::GROUP_AUTH => fn () => $this->reportAuth($resources, $report, $resourceIds), Transfer::GROUP_DATABASES => fn () => $this->reportDatabases($resources, $report, $resourceIds), @@ -315,8 +313,7 @@ public function report(array $resources = [], array $resourceIds = []): array try { $reporter(); } catch (\Throwable $e) { - // Missing scope is user-fixable — collect every affected group and report together. - // Any other failure fails the report rather than returning partial counts. + // Collect missing-scope (403) failures to surface together; any other error fails the report. if ($e->getCode() !== Exception::CODE_FORBIDDEN) { throw $e; } @@ -334,10 +331,7 @@ public function report(array $resources = [], array $resourceIds = []): array return $report; } - /** - * Surface a missing-scope (403) failure from an optional reporter so report() can aggregate it; - * other failures are left for the caller to swallow to a zero count. - */ + /** Re-throw a 403 so report() can aggregate it as a missing scope; the caller swallows other errors. */ private function rethrowIfForbidden(\Throwable $e): void { if ($e->getCode() === Exception::CODE_FORBIDDEN) { From ec09e6dbc477173caffb140feeef2a3f38cdc11a Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 19:49:17 +0100 Subject: [PATCH 5/8] Fail fast on invalid credentials from optional reporters rethrowAuthErrors() now re-throws 401 as well as 403, so an invalid key surfaced only by an optional reporter (platforms/keys/variables/email templates/rules) fails fast instead of returning a zero count. --- src/Migration/Sources/Appwrite.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index d9e29fd4..ad357dec 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -331,10 +331,10 @@ public function report(array $resources = [], array $resourceIds = []): array return $report; } - /** Re-throw a 403 so report() can aggregate it as a missing scope; the caller swallows other errors. */ - private function rethrowIfForbidden(\Throwable $e): void + /** Re-throw credential (401) and scope (403) failures; the caller swallows other errors to zero. */ + private function rethrowAuthErrors(\Throwable $e): void { - if ($e->getCode() === Exception::CODE_FORBIDDEN) { + if (\in_array($e->getCode(), [Exception::CODE_UNAUTHORIZED, Exception::CODE_FORBIDDEN], true)) { throw $e; } } @@ -1678,7 +1678,7 @@ private function reportDomains(array $resources, array &$report, array $resource try { $report[Resource::TYPE_RULE] = $this->proxy->listRules([Query::limit(1)])->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_RULE] = 0; } } @@ -1695,7 +1695,7 @@ private function reportProjects(array $resources, array &$report, array $resourc try { $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_PROJECT_VARIABLE] = 0; } } @@ -1720,7 +1720,7 @@ private function reportProjects(array $resources, array &$report, array $resourc // total:true returns the real count without fetching every row. $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = $this->project->listEmailTemplates([Query::limit(1)], total: true)->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = 0; } } @@ -2877,7 +2877,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_PLATFORM] = $this->project->listPlatforms($platformQueries)->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_PLATFORM] = 0; } } @@ -2891,7 +2891,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_API_KEY] = 0; } } @@ -2905,7 +2905,7 @@ private function reportIntegrations(array $resources, array &$report, array $res try { $report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total; } catch (\Throwable $e) { - $this->rethrowIfForbidden($e); + $this->rethrowAuthErrors($e); $report[Resource::TYPE_WEBHOOK] = 0; } } From bddc5297a6e10b492f15772385f885518de554da Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 19:57:16 +0100 Subject: [PATCH 6/8] Fail fast with a clear message on invalid credentials When an invalid key is first detected by an optional reporter, report() now throws the intended credential error instead of leaking the raw SDK exception. --- src/Migration/Sources/Appwrite.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index ad357dec..bb87dd78 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -313,12 +313,19 @@ public function report(array $resources = [], array $resourceIds = []): array try { $reporter(); } catch (\Throwable $e) { - // Collect missing-scope (403) failures to surface together; any other error fails the report. - if ($e->getCode() !== Exception::CODE_FORBIDDEN) { - throw $e; + $code = $e->getCode(); + + // 403 → collect to surface together; 401 → clear credential error; anything else → fail fast. + if ($code === Exception::CODE_FORBIDDEN) { + $missingScopes[] = $group; + continue; + } + + if ($code === Exception::CODE_UNAUTHORIZED) { + throw new \Exception('Invalid credentials for the migration source.', $code, $e); } - $missingScopes[] = $group; + throw $e; } } From c2febe0ca65babe7458d7fc8584794fb3166a5f2 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 20:14:48 +0100 Subject: [PATCH 7/8] Pass $report by reference so reporter writes are kept Arrow functions capture by value, so each reporter mutated a discarded copy of $report and report() returned only the version. Use first-class callables and pass resources/report/resourceIds as arguments at call time. --- src/Migration/Sources/Appwrite.php | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index bb87dd78..3290cff9 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -294,24 +294,26 @@ public function report(array $resources = [], array $resourceIds = []): array throw new \Exception('Unable to reach the migration source endpoint.', $e->getCode(), $e); } - $groups = [ - Transfer::GROUP_AUTH => fn () => $this->reportAuth($resources, $report, $resourceIds), - Transfer::GROUP_DATABASES => fn () => $this->reportDatabases($resources, $report, $resourceIds), - Transfer::GROUP_STORAGE => fn () => $this->reportStorage($resources, $report, $resourceIds), - Transfer::GROUP_FUNCTIONS => fn () => $this->reportFunctions($resources, $report, $resourceIds), - Transfer::GROUP_MESSAGING => fn () => $this->reportMessaging($resources, $report, $resourceIds), - Transfer::GROUP_SITES => fn () => $this->reportSites($resources, $report, $resourceIds), - Transfer::GROUP_INTEGRATIONS => fn () => $this->reportIntegrations($resources, $report, $resourceIds), - Transfer::GROUP_BACKUPS => fn () => $this->reportBackups($resources, $report, $resourceIds), - Transfer::GROUP_PROJECTS => fn () => $this->reportProjects($resources, $report, $resourceIds), - Transfer::GROUP_DOMAINS => fn () => $this->reportDomains($resources, $report, $resourceIds), + // First-class callables (not fn()) so $report is passed by reference at call time; + // arrow functions would capture a copy and discard each reporter's writes. + $reporters = [ + Transfer::GROUP_AUTH => $this->reportAuth(...), + Transfer::GROUP_DATABASES => $this->reportDatabases(...), + Transfer::GROUP_STORAGE => $this->reportStorage(...), + Transfer::GROUP_FUNCTIONS => $this->reportFunctions(...), + Transfer::GROUP_MESSAGING => $this->reportMessaging(...), + Transfer::GROUP_SITES => $this->reportSites(...), + Transfer::GROUP_INTEGRATIONS => $this->reportIntegrations(...), + Transfer::GROUP_BACKUPS => $this->reportBackups(...), + Transfer::GROUP_PROJECTS => $this->reportProjects(...), + Transfer::GROUP_DOMAINS => $this->reportDomains(...), ]; $missingScopes = []; - foreach ($groups as $group => $reporter) { + foreach ($reporters as $group => $reporter) { try { - $reporter(); + $reporter($resources, $report, $resourceIds); } catch (\Throwable $e) { $code = $e->getCode(); From 6335a882a215fb1deba74ad2666a9db1db98c2d3 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 23 Jun 2026 23:10:33 +0100 Subject: [PATCH 8/8] Stop suppressing Appwrite report failures --- src/Migration/Sources/Appwrite.php | 60 +++++++++--------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 3290cff9..2a94696b 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -317,8 +317,7 @@ public function report(array $resources = [], array $resourceIds = []): array } catch (\Throwable $e) { $code = $e->getCode(); - // 403 → collect to surface together; 401 → clear credential error; anything else → fail fast. - if ($code === Exception::CODE_FORBIDDEN) { + if ($this->isMissingScopeError($e)) { $missingScopes[] = $group; continue; } @@ -340,12 +339,17 @@ public function report(array $resources = [], array $resourceIds = []): array return $report; } - /** Re-throw credential (401) and scope (403) failures; the caller swallows other errors to zero. */ - private function rethrowAuthErrors(\Throwable $e): void + private function isMissingScopeError(\Throwable $e): bool { - if (\in_array($e->getCode(), [Exception::CODE_UNAUTHORIZED, Exception::CODE_FORBIDDEN], true)) { - throw $e; + if (!\in_array($e->getCode(), [Exception::CODE_UNAUTHORIZED, Exception::CODE_FORBIDDEN], true)) { + return false; } + + $message = $e->getMessage(); + + return \str_contains($message, 'general_unauthorized_scope') + || \str_contains($message, 'missing scopes') + || \str_contains($message, 'required scopes'); } /** @@ -1684,12 +1688,7 @@ protected function reportBackups(array $resources, array &$report, array $resour private function reportDomains(array $resources, array &$report, array $resourceIds = []): void { if (\in_array(Resource::TYPE_RULE, $resources)) { - try { - $report[Resource::TYPE_RULE] = $this->proxy->listRules([Query::limit(1)])->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_RULE] = 0; - } + $report[Resource::TYPE_RULE] = $this->proxy->listRules([Query::limit(1)])->total; } } @@ -1701,12 +1700,7 @@ private function reportProjects(array $resources, array &$report, array $resourc resourceIds: $resourceIds, limit: 1 ); - try { - $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_PROJECT_VARIABLE] = 0; - } + $report[Resource::TYPE_PROJECT_VARIABLE] = $this->project->listVariables($variableQueries)->total; } if (\in_array(Resource::TYPE_PROJECT_PROTOCOLS, $resources)) { @@ -1725,13 +1719,8 @@ private function reportProjects(array $resources, array &$report, array $resourc } if (\in_array(Resource::TYPE_PROJECT_EMAIL_TEMPLATE, $resources)) { - try { - // total:true returns the real count without fetching every row. - $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = $this->project->listEmailTemplates([Query::limit(1)], total: true)->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = 0; - } + // total:true returns the real count without fetching every row. + $report[Resource::TYPE_PROJECT_EMAIL_TEMPLATE] = $this->project->listEmailTemplates([Query::limit(1)], total: true)->total; } } @@ -2883,12 +2872,7 @@ private function reportIntegrations(array $resources, array &$report, array $res resourceIds: $resourceIds, limit: 1 ); - try { - $report[Resource::TYPE_PLATFORM] = $this->project->listPlatforms($platformQueries)->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_PLATFORM] = 0; - } + $report[Resource::TYPE_PLATFORM] = $this->project->listPlatforms($platformQueries)->total; } if (\in_array(Resource::TYPE_API_KEY, $resources)) { @@ -2897,12 +2881,7 @@ private function reportIntegrations(array $resources, array &$report, array $res resourceIds: $resourceIds, limit: 1 ); - try { - $report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_API_KEY] = 0; - } + $report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total; } if (\in_array(Resource::TYPE_WEBHOOK, $resources)) { @@ -2911,12 +2890,7 @@ private function reportIntegrations(array $resources, array &$report, array $res resourceIds: $resourceIds, limit: 1 ); - try { - $report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total; - } catch (\Throwable $e) { - $this->rethrowAuthErrors($e); - $report[Resource::TYPE_WEBHOOK] = 0; - } + $report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total; } if (\in_array(Resource::TYPE_SMTP, $resources)) {