From a6d58a847d6a0262848f578e8c85857d7eb52cff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 13:15:26 +0530 Subject: [PATCH 01/26] fix: handle beta SDK versioning in DiffCheck prompt Beta SDKs (version < 1.0.0) should not be bumped to 1.0.0. The prompt now checks the beta flag from sdk config and instructs the AI to use minor bumps for both breaking changes and new features, and patch for bug fixes only. Also adds a rule to wrap code identifiers in backticks for better changelog rendering. --- src/Appwrite/Platform/Tasks/SDKs.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 949dbb3e6b..f2103893a9 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -780,20 +780,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND required: $object->getNames() ); + $isBeta = !empty($language['beta']); + $betaNote = $isBeta + ? "\n Note: This SDK is in beta (version < 1.0.0). Do NOT bump to 1.0.0. Use `minor` for both breaking changes and new features, `patch` for bug fixes only." + : ''; + $prompt = << Date: Wed, 4 Mar 2026 13:16:55 +0530 Subject: [PATCH 02/26] refactor: derive supported SDKs from config instead of hardcoded array Replace the static supportedSDKS array with a getSupportedSDKs() method that reads SDK keys from the sdks.php config file. This eliminates the need to maintain the list in two places. --- src/Appwrite/Platform/Tasks/SDKs.php | 38 ++++++++++------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index f2103893a9..27095e6ec2 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -41,28 +41,17 @@ use Utopia\Validator\WhiteList; class SDKs extends Action { - protected array $supportedSDKS = [ - 'web', - 'cli', - 'php', - 'nodejs', - 'deno', - 'python', - 'ruby', - 'flutter', - 'react-native', - 'dart', - 'go', - 'swift', - 'apple', - 'dotnet', - 'android', - 'graphql', - 'rest', - 'markdown', - 'agent-skills', - 'cursor-plugin' - ]; + protected function getSupportedSDKs(): array + { + $keys = []; + $platforms = Config::getParam('sdks'); + foreach ($platforms as $platform) { + foreach ($platform['sdks'] as $sdk) { + $keys[] = $sdk['key']; + } + } + return \array_unique($keys); + } public static function getName(): string { @@ -100,8 +89,9 @@ class SDKs extends Action if (! $sdks) { $selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):'); $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); - if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $this->supportedSDKS)) { - throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $this->supportedSDKS)); + $supportedSDKs = $this->getSupportedSDKs(); + if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $supportedSDKs)) { + throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $supportedSDKs)); } } else { $sdks = explode(',', $sdks); From fc88d4b4ab9039aed465fb7e5d581f70f4aff2b1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 13:19:48 +0530 Subject: [PATCH 03/26] feat: auto-generate commit messages for SDK pushes Remove the manual commit message prompt. When AI is available and produces a changelog, use it as the commit message. Otherwise fall back to a descriptive message based on SDK name and version. A manually provided --message flag still takes priority. --- src/Appwrite/Platform/Tasks/SDKs.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 27095e6ec2..5aa5042739 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -107,9 +107,6 @@ class SDKs extends Action $prUrls = []; - if ($git) { - $message ??= Console::confirm('Please enter your commit message:'); - } } elseif ($examplesOnly) { $git = false; $prUrls = []; @@ -518,6 +515,16 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repoBranch = $language['repoBranch'] ?? 'main'; if ($git && ! empty($gitUrl)) { + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; + } else { + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; + } + $escapedCommitMessage = \addcslashes($commitMessage, '"\\`$'); + Console::info("Preparing {$language['name']} SDK repository..."); \exec('rm -rf ' . $target . ' && \ @@ -540,7 +547,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND cp -r ' . $result . '/. ' . $target . '/ && \ (if [ -d /tmp/.github-backup-$$/.github ]; then cp -rn /tmp/.github-backup-$$/.github . 2>/dev/null && rm -rf /tmp/.github-backup-$$; fi) && \ git add -A && \ - git commit -m "' . $message . '" --quiet && \ + git commit -m "' . $escapedCommitMessage . '" --quiet && \ git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true ', $gitOutput, $gitReturnCode); From f989ccde579f6796300f2737a2df5546afbde1f0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 18:44:29 +0530 Subject: [PATCH 04/26] fix: add post-parse guard for beta SDK version bumps Add a programmatic guard after parsing the AI response that rejects major bumps or versions >= 1.0.0 for beta SDKs. When triggered, the SDK is skipped with a warning instead of proceeding with an invalid version. --- src/Appwrite/Platform/Tasks/SDKs.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 5aa5042739..715d322892 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -878,6 +878,12 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return null; } + // Guard: beta SDKs must not be bumped to >= 1.0.0 + if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) { + Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping"); + return null; + } + Console::success("✓ Analysis complete"); Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)"); Console::log(" Changelog:"); From 300aaeb25136fce0dcae9d2848eab9b8d13111ae Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 18:46:49 +0530 Subject: [PATCH 05/26] fix: escape shell arguments in SDK git and gh CLI commands Replace raw string interpolation with escapeshellarg() for all arguments passed to exec/shell_exec calls that build git commit, gh pr create, gh api, and gh release commands. This prevents shell injection from AI-generated changelog text or any other dynamically constructed values. --- src/Appwrite/Platform/Tasks/SDKs.php | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 715d322892..a7e9d2c861 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -324,7 +324,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } // Check if release already exists - $checkReleaseCommand = 'gh release view "' . $releaseVersion . '" --repo "' . $repoName . '" --json url --jq ".url" 2>/dev/null'; + $checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null'; $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); if (! empty($existingReleaseUrl)) { @@ -355,7 +355,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } $previousVersion = ''; - $tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; + $tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; $previousVersion = trim(\shell_exec($tagListCommand) ?? ''); $formattedNotes = "## What's Changed\n\n"; @@ -383,11 +383,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); \file_put_contents($tempNotesFile, $formattedNotes); - $releaseCommand = 'gh release create "' . $releaseVersion . '" \ - --repo "' . $repoName . '" \ - --title "' . $releaseTitle . '" \ - --notes-file "' . $tempNotesFile . '" \ - --target "' . $releaseTarget . '" \ + $releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($releaseTitle) . ' \ + --notes-file ' . \escapeshellarg($tempNotesFile) . ' \ + --target ' . \escapeshellarg($releaseTarget) . ' \ 2>&1'; $releaseOutput = []; @@ -523,8 +523,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } else { $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; } - $escapedCommitMessage = \addcslashes($commitMessage, '"\\`$'); - Console::info("Preparing {$language['name']} SDK repository..."); \exec('rm -rf ' . $target . ' && \ @@ -547,7 +545,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND cp -r ' . $result . '/. ' . $target . '/ && \ (if [ -d /tmp/.github-backup-$$/.github ]; then cp -rn /tmp/.github-backup-$$/.github . 2>/dev/null && rm -rf /tmp/.github-backup-$$; fi) && \ git add -A && \ - git commit -m "' . $escapedCommitMessage . '" --quiet && \ + git commit -m ' . \escapeshellarg($commitMessage) . ' --quiet && \ git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true ', $gitOutput, $gitReturnCode); @@ -570,11 +568,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prCommand = 'cd ' . $target . ' && \ gh pr create \ - --repo "' . $repoName . '" \ - --title "' . $prTitle . '" \ - --body "' . $prBody . '" \ - --base "' . $repoBranch . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ 2>&1'; $prOutput = []; @@ -592,8 +590,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); $prNumberCommand = 'cd ' . $target . ' && \ gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ --json number \ --jq ".[0].number" \ 2>&1'; @@ -606,14 +604,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prNumber = trim($prNumberOutput[0]); // Use API directly to update PR to avoid deprecated projectCards field + $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; $updateCommand = 'cd ' . $target . ' && \ gh api \ --method PATCH \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/' . $repoName . '/pulls/' . $prNumber . ' \ - -f title="' . $prTitle . '" \ - -f body="' . $prBody . '" \ + ' . \escapeshellarg($apiPath) . ' \ + -f title=' . \escapeshellarg($prTitle) . ' \ + -f body=' . \escapeshellarg($prBody) . ' \ 2>&1'; $updateOutput = []; @@ -625,8 +624,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prUrlCommand = 'cd ' . $target . ' && \ gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ --json url \ --jq ".[0].url" \ 2>&1'; From 198f9a64a30afb6648edab3d641201cd77a4d187 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:03:21 +0530 Subject: [PATCH 06/26] refactoring --- composer.json | 1 + composer.lock | 68 ++++- docker-compose.yml | 18 -- src/Appwrite/Platform/Tasks/SDKs.php | 388 +++++++++++++++------------ 4 files changed, 285 insertions(+), 190 deletions(-) diff --git a/composer.json b/composer.json index 1422bd5d0a..d7f0ae66b5 100644 --- a/composer.json +++ b/composer.json @@ -100,6 +100,7 @@ "swoole/ide-helper": "6.*", "phpstan/phpstan": "1.12.*", "textalk/websocket": "1.5.*", + "czproject/git-php": "4.*", "laravel/pint": "1.*", "phpbench/phpbench": "1.*" }, diff --git a/composer.lock b/composer.lock index ccf03ce835..6097b8ecd9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1fb043a556550f62ab27a0aad0b9332a", + "content-hash": "1cc64e07484256225f56bd525674c3b8", "packages": [ { "name": "adhocore/jwt", @@ -5580,6 +5580,70 @@ ], "time": "2026-02-25T14:53:45+00:00" }, + { + "name": "czproject/git-php", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/czproject/git-php/issues", + "source": "https://github.com/czproject/git-php/tree/v4.6.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/janpecha", + "type": "github" + }, + { + "url": "https://www.janpecha.cz/donate/git-php/", + "type": "other" + }, + { + "url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA", + "type": "stripe" + }, + { + "url": "https://thanks.dev/u/gh/czproject", + "type": "thanks.dev" + } + ], + "time": "2025-11-10T07:24:07+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.2", @@ -9043,7 +9107,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index bdf77a46b4..4a38757737 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -152,7 +152,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE @@ -305,7 +304,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_USAGE_STATS - _APP_LOGGING_CONFIG - _APP_LOGGING_CONFIG_REALTIME @@ -340,7 +338,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -371,7 +368,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -414,7 +410,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_STORAGE_DEVICE - _APP_STORAGE_S3_ACCESS_KEY - _APP_STORAGE_S3_SECRET @@ -474,7 +469,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_WORKERS_NUM - _APP_QUEUE_NAME @@ -513,7 +507,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_VCS_GITHUB_APP_NAME - _APP_VCS_GITHUB_PRIVATE_KEY @@ -658,7 +651,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -722,7 +714,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_FUNCTIONS_TIMEOUT - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT @@ -808,7 +799,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_SMS_FROM - _APP_SMS_PROVIDER @@ -875,7 +865,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET @@ -918,7 +907,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_CACHE @@ -994,7 +982,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1028,7 +1015,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1062,7 +1048,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1100,7 +1085,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-task-scheduler-executions: @@ -1131,7 +1115,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER appwrite-task-scheduler-messages: entrypoint: schedule-messages @@ -1161,7 +1144,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-assistant: diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index a7e9d2c861..c318e34686 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -25,6 +25,7 @@ use Appwrite\SDK\Language\Swift; use Appwrite\SDK\Language\Web; use Appwrite\SDK\SDK; use Appwrite\Spec\Swagger2; +use CzProject\GitPhp\Git; use Utopia\Agents\Adapters\OpenAI; use Utopia\Agents\DiffCheck\DiffCheck; use Utopia\Agents\DiffCheck\Options as DiffCheckOptions; @@ -41,18 +42,6 @@ use Utopia\Validator\WhiteList; class SDKs extends Action { - protected function getSupportedSDKs(): array - { - $keys = []; - $platforms = Config::getParam('sdks'); - foreach ($platforms as $platform) { - foreach ($platform['sdks'] as $sdk) { - $keys[] = $sdk['key']; - } - } - return \array_unique($keys); - } - public static function getName(): string { return 'sdks'; @@ -63,6 +52,19 @@ class SDKs extends Action return Specs::getPlatforms(); } + protected function getSdkConfigPath(): string + { + return __DIR__ . '/../../../../app/config/sdks.php'; + } + + protected function getSupportedSDKs(): array + { + return \array_unique(\array_merge(...\array_map( + fn ($platform) => \array_column($platform['sdks'], 'key'), + Config::getParam('sdks') + ))); + } + public function __construct() { $this @@ -514,146 +516,151 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitBranch = $language['gitBranch']; $repoBranch = $language['repoBranch'] ?? 'main'; - if ($git && ! empty($gitUrl)) { - // Generate commit message: use provided message, AI changelog, or fallback - if (! empty($message)) { - $commitMessage = $message; - } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; - } else { - $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; - } - Console::info("Preparing {$language['name']} SDK repository..."); - - \exec('rm -rf ' . $target . ' && \ - mkdir -p ' . $target . ' && \ - cd ' . $target . ' && \ - git init --quiet && \ - git config core.ignorecase false && \ - git config pull.rebase false && \ - git config advice.defaultBranchName false && \ - git remote add origin ' . $gitUrl . ' && \ - git fetch origin --quiet --no-tags --depth 1 ' . $repoBranch . ' 2>&1 | grep -v "^remote:" | grep -v "^From " | grep -v "^ \* " || true && \ - (git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \ - git pull origin ' . $repoBranch . ' --quiet --no-tags 2>&1 | grep -v "^From " | grep -v "^ \* " || true && \ - (git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \ - (git fetch origin ' . $gitBranch . ' --quiet --no-tags --depth 1 2>/dev/null || git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -v "^remote:" || true) && \ - git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \ - (if [ -d .github ]; then cp -r .github /tmp/.github-backup-$$ 2>/dev/null; fi) && \ - git rm -rf --cached . 2>/dev/null && \ - git clean -fdx -e .git -e .github 2>/dev/null && \ - cp -r ' . $result . '/. ' . $target . '/ && \ - (if [ -d /tmp/.github-backup-$$/.github ]; then cp -rn /tmp/.github-backup-$$/.github . 2>/dev/null && rm -rf /tmp/.github-backup-$$; fi) && \ - git add -A && \ - git commit -m ' . \escapeshellarg($commitMessage) . ' --quiet && \ - git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true - ', $gitOutput, $gitReturnCode); - - if ($gitReturnCode !== 0) { - Console::warning("Git operations completed with warnings (exit code: {$gitReturnCode})"); - } - - Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - if ($git) { - $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; - - // Build PR body with AI changelog if available - $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; - if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; - } - $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; - - Console::info("Creating pull request for {$language['name']} SDK..."); - - $prCommand = 'cd ' . $target . ' && \ - gh pr create \ - --repo ' . \escapeshellarg($repoName) . ' \ - --title ' . \escapeshellarg($prTitle) . ' \ - --body ' . \escapeshellarg($prBody) . ' \ - --base ' . \escapeshellarg($repoBranch) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - 2>&1'; - - $prOutput = []; - $prReturnCode = 0; - \exec($prCommand, $prOutput, $prReturnCode); - - if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); - if (! empty($prOutput)) { - $prUrls[$language['name']] = end($prOutput); - } - } else { - $errorMessage = implode("\n", $prOutput); - if (strpos($errorMessage, 'already exists') !== false) { - Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); - $prNumberCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo ' . \escapeshellarg($repoName) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - --json number \ - --jq ".[0].number" \ - 2>&1'; - - $prNumberOutput = []; - $prNumberReturnCode = 0; - \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); - - if ($prNumberReturnCode === 0 && ! empty($prNumberOutput[0])) { - $prNumber = trim($prNumberOutput[0]); - - // Use API directly to update PR to avoid deprecated projectCards field - $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; - $updateCommand = 'cd ' . $target . ' && \ - gh api \ - --method PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - ' . \escapeshellarg($apiPath) . ' \ - -f title=' . \escapeshellarg($prTitle) . ' \ - -f body=' . \escapeshellarg($prBody) . ' \ - 2>&1'; - - $updateOutput = []; - $updateReturnCode = 0; - \exec($updateCommand, $updateOutput, $updateReturnCode); - - if ($updateReturnCode === 0) { - Console::success("Successfully updated pull request for {$language['name']} SDK"); - - $prUrlCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo ' . \escapeshellarg($repoName) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - --json url \ - --jq ".[0].url" \ - 2>&1'; - - $prUrlOutput = []; - $prUrlReturnCode = 0; - \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); - - if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { - $prUrls[$language['name']] = trim($prUrlOutput[0]); - } - } else { - $updateErrorMessage = implode("\n", $updateOutput); - Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage); - } - } else { - Console::error("Failed to get PR number for {$language['name']} SDK"); - } - } else { - Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); - } - } - } - - \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); - Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); + if (! $git || empty($gitUrl)) { + goto copyExamples; } + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; + } else { + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; + } + Console::info("Preparing {$language['name']} SDK repository..."); + + try { + // Init fresh repo + \exec('rm -rf ' . \escapeshellarg($target)); + \mkdir($target, 0755, true); + + $gitClient = new Git(); + $repo = $gitClient->init($target); + + $repo->execute('config', 'core.ignorecase', 'false'); + $repo->execute('config', 'pull.rebase', 'false'); + $repo->execute('config', 'advice.defaultBranchName', 'false'); + $repo->addRemote('origin', $gitUrl); + + // Fetch and checkout base branch (or create if new repo) + try { + $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); + try { + $repo->execute('checkout', '-f', $repoBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + + try { + $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); + } catch (\Throwable) { + } + + // Checkout dev branch (or create if it doesn't exist) + try { + $repo->execute('checkout', '-f', $gitBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $gitBranch); + } + + // Fetch dev branch, or push to create it on remote + try { + $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); + } catch (\Throwable) { + try { + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable) { + } + } + + // Sync with remote dev branch + try { + $repo->execute('reset', '--hard', "origin/{$gitBranch}"); + } catch (\Throwable) { + } + + // Backup .github before cleaning working tree + $githubDir = $target . '/.github'; + $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); + $hasGithubDir = \is_dir($githubDir); + if ($hasGithubDir) { + \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); + } + + // Clean working tree + try { + $repo->execute('rm', '-rf', '--cached', '.'); + } catch (\Throwable) { + } + try { + $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); + } catch (\Throwable) { + } + + // Copy generated SDK and restore .github + \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); + + if ($hasGithubDir && \is_dir($githubBackup)) { + \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); + \exec('rm -rf ' . \escapeshellarg($githubBackup)); + } + + // Stage, commit, push + $repo->addAllChanges(); + $repo->commit($commitMessage); + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable $e) { + Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + } + + Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + + // Create or update pull request + $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; + if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; + } + $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; + + Console::info("Creating pull request for {$language['name']} SDK..."); + + $prCommand = 'cd ' . $target . ' && \ + gh pr create \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + 2>&1'; + + $prOutput = []; + $prReturnCode = 0; + \exec($prCommand, $prOutput, $prReturnCode); + + if ($prReturnCode === 0) { + Console::success("Successfully created pull request for {$language['name']} SDK"); + if (! empty($prOutput)) { + $prUrls[$language['name']] = end($prOutput); + } + } else { + $errorMessage = implode("\n", $prOutput); + if (strpos($errorMessage, 'already exists') === false) { + Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + } else { + $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + } + } + + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); + + copyExamples: + $docDirectories = $language['docDirectories'] ?? ['']; if ($version === 'latest') { @@ -858,7 +865,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (empty(trim($responseContent))) { Console::warning('AI returned empty response'); - return null; } @@ -868,7 +874,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg()); Console::log('Raw response:'); Console::log($responseContent); - return null; } @@ -899,21 +904,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ]; } catch (\Throwable $e) { Console::error('Error generating version and changelog: ' . $e->getMessage()); - return null; } } - /** - * Get the SDK config file path - * - * @return string Path to the SDK config file - */ - protected function getSdkConfigPath(): string - { - return __DIR__ . '/../../../../app/config/sdks.php'; - } - /** * Update SDK version in the config file * @@ -928,7 +922,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (! file_exists($configPath)) { Console::error("Config file not found: {$configPath}"); - return false; } @@ -944,11 +937,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($configPath, $newContent) !== false) { Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); - return true; } else { Console::error('Failed to write config file'); - return false; } } @@ -964,11 +955,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($configPath, $newContent) !== false) { Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); - return true; } else { Console::error('Failed to write config file'); - return false; } } @@ -1027,12 +1016,71 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($changelogPath, $newContent) !== false) { Console::success("Updated changelog at {$changelogPath} with version {$version}"); - return true; } else { Console::error('Failed to write changelog file'); - return false; } } + + private function updateExistingPr(string $target, string $repoName, string $gitBranch, string $prTitle, string $prBody, string $sdkName, array &$prUrls): void + { + Console::warning("Pull request already exists for {$sdkName} SDK, updating title and body..."); + + $prNumberCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json number \ + --jq ".[0].number" \ + 2>&1'; + + $prNumberOutput = []; + $prNumberReturnCode = 0; + \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); + + if ($prNumberReturnCode !== 0 || empty($prNumberOutput[0])) { + Console::error("Failed to get PR number for {$sdkName} SDK"); + return; + } + + $prNumber = trim($prNumberOutput[0]); + $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; + $updateCommand = 'cd ' . $target . ' && \ + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ' . \escapeshellarg($apiPath) . ' \ + -f title=' . \escapeshellarg($prTitle) . ' \ + -f body=' . \escapeshellarg($prBody) . ' \ + 2>&1'; + + $updateOutput = []; + $updateReturnCode = 0; + \exec($updateCommand, $updateOutput, $updateReturnCode); + + if ($updateReturnCode !== 0) { + Console::error("Failed to update pull request for {$sdkName} SDK: " . implode("\n", $updateOutput)); + return; + } + + Console::success("Successfully updated pull request for {$sdkName} SDK"); + + $prUrlCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json url \ + --jq ".[0].url" \ + 2>&1'; + + $prUrlOutput = []; + $prUrlReturnCode = 0; + \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); + + if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { + $prUrls[$sdkName] = trim($prUrlOutput[0]); + } + } } From 50f9c6786212441eca886a90d63c7fa6afea9a62 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:10:39 +0530 Subject: [PATCH 07/26] fixes --- app/config/sdks.php | 2 +- src/Appwrite/Platform/Tasks/SDKs.php | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/config/sdks.php b/app/config/sdks.php index 77db161b5c..1a808aa10a 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -231,7 +231,7 @@ return [ 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://www.npmjs.com/package/appwrite-cli', 'enabled' => true, - 'beta' => true, + 'beta' => false, 'dev' => false, 'hidden' => false, 'family' => APP_SDK_PLATFORM_CONSOLE, diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index c318e34686..ceb07d25cb 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -59,10 +59,10 @@ class SDKs extends Action protected function getSupportedSDKs(): array { - return \array_unique(\array_merge(...\array_map( + return \array_unique(\array_merge(...\array_values(\array_map( fn ($platform) => \array_column($platform['sdks'], 'key'), Config::getParam('sdks') - ))); + )))); } public function __construct() @@ -480,7 +480,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::info("Analyzing SDK changes with AI..."); $aiResult = $this->generateVersionAndChangelog($language, $result); - if ($aiResult !== null) { + if (!empty($aiResult['skip'])) { + Console::warning("Skipping {$language['name']} SDK generation"); + continue; + } elseif ($aiResult !== null) { $newVersion = $aiResult['version']; $newChangelog = $aiResult['changelog']; $aiChangelog = $newChangelog; // Store for PR description @@ -885,7 +888,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Guard: beta SDKs must not be bumped to >= 1.0.0 if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) { Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping"); - return null; + return ['skip' => true]; } Console::success("✓ Analysis complete"); From c497a686133738405315d78592b0330297db8167 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:33:41 +0530 Subject: [PATCH 08/26] fixes --- src/Appwrite/Platform/Tasks/SDKs.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index ceb07d25cb..40874c4879 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -491,19 +491,17 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Update the version in the config $this->updateSdkVersion($key, $language['key'], $newVersion); - // Update the changelog file + // Update the source changelog file $this->updateChangelogFile($language['changelog'], $newVersion, $newChangelog); - // Also update CHANGELOG.md in the generated SDK directory - $sdkChangelogPath = $result . '/CHANGELOG.md'; - if (file_exists($sdkChangelogPath)) { - $this->updateChangelogFile($sdkChangelogPath, $newVersion, $newChangelog); - } + // Re-read updated changelog so regeneration includes the new entry + $updatedChangelog = \file_get_contents($language['changelog']); + $sdk->setChangelog($updatedChangelog); // Reload the language config with updated values $language['version'] = $newVersion; - // Regenerate SDK with new version + // Regenerate SDK with new version and updated changelog $sdk->setVersion($newVersion); try { $sdk->generate($result); @@ -647,8 +645,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if ($prReturnCode === 0) { Console::success("Successfully created pull request for {$language['name']} SDK"); - if (! empty($prOutput)) { - $prUrls[$language['name']] = end($prOutput); + foreach ($prOutput as $line) { + if (\str_starts_with(trim($line), 'https://')) { + $prUrls[$language['name']] = trim($line); + break; + } } } else { $errorMessage = implode("\n", $prOutput); From 161c5af66f8b49f946eb8629c59d1532ac341e5b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:35:30 +0530 Subject: [PATCH 09/26] fixes --- src/Appwrite/Platform/Tasks/SDKs.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 40874c4879..c9875e1194 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -616,6 +616,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); } catch (\Throwable $e) { Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + goto cleanupTarget; } Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); @@ -660,6 +661,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + cleanupTarget: \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); From d1ffa5daf3d966e947115c1726d809aa3909494d Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 4 Mar 2026 08:25:05 +0000 Subject: [PATCH 10/26] chore: bump utopia-php/migration to 1.6.3 --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index ccf03ce835..d29a544755 100644 --- a/composer.lock +++ b/composer.lock @@ -4517,16 +4517,16 @@ }, { "name": "utopia-php/migration", - "version": "1.6.2", + "version": "1.6.3", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca" + "reference": "c2d016944cb029fa5ff822ceee704785a06ef289" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/037bf4b3813d44f1b0990bc124e35b501ed27fca", - "reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c2d016944cb029fa5ff822ceee704785a06ef289", + "reference": "c2d016944cb029fa5ff822ceee704785a06ef289", "shasum": "" }, "require": { @@ -4566,9 +4566,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.6.2" + "source": "https://github.com/utopia-php/migration/tree/1.6.3" }, - "time": "2026-02-25T12:00:11+00:00" + "time": "2026-03-04T07:08:22+00:00" }, { "name": "utopia-php/mongo", From 152de6c58488d4678ef4a243f79d6238086865cf Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 5 Mar 2026 09:32:07 +0530 Subject: [PATCH 11/26] remove goto --- src/Appwrite/Platform/Tasks/SDKs.php | 354 ++++++++++++++------------- 1 file changed, 186 insertions(+), 168 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index c9875e1194..528084f4ea 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -517,178 +517,26 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitBranch = $language['gitBranch']; $repoBranch = $language['repoBranch'] ?? 'main'; - if (! $git || empty($gitUrl)) { - goto copyExamples; - } - - // Generate commit message: use provided message, AI changelog, or fallback - if (! empty($message)) { - $commitMessage = $message; - } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; - } else { - $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; - } - Console::info("Preparing {$language['name']} SDK repository..."); - - try { - // Init fresh repo - \exec('rm -rf ' . \escapeshellarg($target)); - \mkdir($target, 0755, true); - - $gitClient = new Git(); - $repo = $gitClient->init($target); - - $repo->execute('config', 'core.ignorecase', 'false'); - $repo->execute('config', 'pull.rebase', 'false'); - $repo->execute('config', 'advice.defaultBranchName', 'false'); - $repo->addRemote('origin', $gitUrl); - - // Fetch and checkout base branch (or create if new repo) - try { - $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); - try { - $repo->execute('checkout', '-f', $repoBranch); - } catch (\Throwable) { - $repo->execute('checkout', '-b', $repoBranch); - } - } catch (\Throwable) { - $repo->execute('checkout', '-b', $repoBranch); - } - - try { - $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); - } catch (\Throwable) { - } - - // Checkout dev branch (or create if it doesn't exist) - try { - $repo->execute('checkout', '-f', $gitBranch); - } catch (\Throwable) { - $repo->execute('checkout', '-b', $gitBranch); - } - - // Fetch dev branch, or push to create it on remote - try { - $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); - } catch (\Throwable) { - try { - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); - } catch (\Throwable) { - } - } - - // Sync with remote dev branch - try { - $repo->execute('reset', '--hard', "origin/{$gitBranch}"); - } catch (\Throwable) { - } - - // Backup .github before cleaning working tree - $githubDir = $target . '/.github'; - $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); - $hasGithubDir = \is_dir($githubDir); - if ($hasGithubDir) { - \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); - } - - // Clean working tree - try { - $repo->execute('rm', '-rf', '--cached', '.'); - } catch (\Throwable) { - } - try { - $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); - } catch (\Throwable) { - } - - // Copy generated SDK and restore .github - \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); - - if ($hasGithubDir && \is_dir($githubBackup)) { - \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); - \exec('rm -rf ' . \escapeshellarg($githubBackup)); - } - - // Stage, commit, push - $repo->addAllChanges(); - $repo->commit($commitMessage); - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); - } catch (\Throwable $e) { - Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); - goto cleanupTarget; - } - - Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - - // Create or update pull request - $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; - $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; - if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; - } - $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; - - Console::info("Creating pull request for {$language['name']} SDK..."); - - $prCommand = 'cd ' . $target . ' && \ - gh pr create \ - --repo ' . \escapeshellarg($repoName) . ' \ - --title ' . \escapeshellarg($prTitle) . ' \ - --body ' . \escapeshellarg($prBody) . ' \ - --base ' . \escapeshellarg($repoBranch) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - 2>&1'; - - $prOutput = []; - $prReturnCode = 0; - \exec($prCommand, $prOutput, $prReturnCode); - - if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); - foreach ($prOutput as $line) { - if (\str_starts_with(trim($line), 'https://')) { - $prUrls[$language['name']] = trim($line); - break; - } - } - } else { - $errorMessage = implode("\n", $prOutput); - if (strpos($errorMessage, 'already exists') === false) { - Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + if ($git && !empty($gitUrl)) { + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; } else { - $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); - } - } - - cleanupTarget: - \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); - Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); - - copyExamples: - - $docDirectories = $language['docDirectories'] ?? ['']; - - if ($version === 'latest') { - continue; - } - - foreach ($docDirectories as $languageTitle => $path) { - $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); - $examplesSource = $result . '/docs/examples' . $languagePath; - - if (! \is_dir($examplesSource)) { - Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); - - continue; + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; } - \exec( - 'mkdir -p ' . $resultExamples . $languagePath . ' && \ - cp -r ' . $examplesSource . ' ' . $resultExamples - ); - Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + $pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage); + + if ($pushSuccess) { + $this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls); + } + + $this->cleanupTarget($target, $language['name']); } + + $this->copyExamples($language, $version, $result, $resultExamples); } } @@ -702,6 +550,176 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + private function pushToGit(array $language, string $target, string $result, string $gitUrl, string $gitBranch, string $repoBranch, string $commitMessage): bool + { + Console::info("Preparing {$language['name']} SDK repository..."); + + try { + // Init fresh repo + \exec('rm -rf ' . \escapeshellarg($target)); + \mkdir($target, 0755, true); + + $gitClient = new Git(); + $repo = $gitClient->init($target); + + $repo->execute('config', 'core.ignorecase', 'false'); + $repo->execute('config', 'pull.rebase', 'false'); + $repo->execute('config', 'advice.defaultBranchName', 'false'); + $repo->addRemote('origin', $gitUrl); + + // Fetch and checkout base branch (or create if new repo) + try { + $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); + try { + $repo->execute('checkout', '-f', $repoBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + + try { + $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); + } catch (\Throwable) { + } + + // Checkout dev branch (or create if it doesn't exist) + try { + $repo->execute('checkout', '-f', $gitBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $gitBranch); + } + + // Fetch dev branch, or push to create it on remote + try { + $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); + } catch (\Throwable) { + try { + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable) { + } + } + + // Sync with remote dev branch + try { + $repo->execute('reset', '--hard', "origin/{$gitBranch}"); + } catch (\Throwable) { + } + + // Backup .github before cleaning working tree + $githubDir = $target . '/.github'; + $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); + $hasGithubDir = \is_dir($githubDir); + if ($hasGithubDir) { + \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); + } + + // Clean working tree + try { + $repo->execute('rm', '-rf', '--cached', '.'); + } catch (\Throwable) { + } + try { + $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); + } catch (\Throwable) { + } + + // Copy generated SDK and restore .github + \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); + + if ($hasGithubDir && \is_dir($githubBackup)) { + \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); + \exec('rm -rf ' . \escapeshellarg($githubBackup)); + } + + // Stage, commit, push + $repo->addAllChanges(); + $repo->commit($commitMessage); + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable $e) { + Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + return false; + } + + Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + return true; + } + + private function createPullRequest(array $language, string $target, string $gitBranch, string $repoBranch, string $aiChangelog, array &$prUrls): void + { + $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; + if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; + } + $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; + + Console::info("Creating pull request for {$language['name']} SDK..."); + + $prCommand = 'cd ' . $target . ' && \ + gh pr create \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + 2>&1'; + + $prOutput = []; + $prReturnCode = 0; + \exec($prCommand, $prOutput, $prReturnCode); + + if ($prReturnCode === 0) { + Console::success("Successfully created pull request for {$language['name']} SDK"); + foreach ($prOutput as $line) { + if (\str_starts_with(trim($line), 'https://')) { + $prUrls[$language['name']] = trim($line); + break; + } + } + } else { + $errorMessage = implode("\n", $prOutput); + if (strpos($errorMessage, 'already exists') === false) { + Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + } else { + $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + } + } + } + + private function cleanupTarget(string $target, string $languageName): void + { + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::success("Remove temp directory '{$target}' for {$languageName} SDK"); + } + + private function copyExamples(array $language, string $version, string $result, string $resultExamples): void + { + $docDirectories = $language['docDirectories'] ?? ['']; + + if ($version === 'latest') { + return; + } + + foreach ($docDirectories as $languageTitle => $path) { + $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); + $examplesSource = $result . '/docs/examples' . $languagePath; + + if (! \is_dir($examplesSource)) { + Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); + + continue; + } + + \exec( + 'mkdir -p ' . $resultExamples . $languagePath . ' && \ + cp -r ' . $examplesSource . ' ' . $resultExamples + ); + Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + } + } + /** * Extract release notes from changelog for a specific version */ From 9d2036024aad199eb7bcc604a23095894424589d Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 5 Mar 2026 08:24:32 +0100 Subject: [PATCH 12/26] Refactor migration error handling to provide clearer connection error messages. Updated exception messages for migration sources to guide users on potential credential and network issues. --- app/controllers/api/migrations.php | 69 +++++++++--------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 5a864ab928..bfb73189b5 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -717,22 +717,14 @@ Http::get('/v1/migrations/appwrite/report') ->inject('project') ->inject('user') ->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) { - - $appwrite = new Appwrite($projectID, $endpoint, $key); - try { + $appwrite = new Appwrite($projectID, $endpoint, $key); $report = $appwrite->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -771,21 +763,14 @@ Http::get('/v1/migrations/firebase/report') throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON'); } - $firebase = new Firebase($serviceAccount); - try { + $firebase = new Firebase($serviceAccount); $report = $firebase->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -820,21 +805,14 @@ Http::get('/v1/migrations/supabase/report') ->inject('response') ->inject('dbForProject') ->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) { - $supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port); - try { + $supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port); $report = $supabase->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -869,21 +847,14 @@ Http::get('/v1/migrations/nhost/report') ->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true) ->inject('response') ->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) { - $nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port); - try { + $nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port); $report = $nhost->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response From d7722204148b366d197db6bb6cbbf117ff248d1b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Mar 2026 10:28:42 +0200 Subject: [PATCH 13/26] stopOnError --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index e2876bb486..030d89af8d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ colors="true" processIsolation="false" stopOnFailure="false" + stopOnError="false" cacheDirectory=".phpunit.cache" > From 5821832da667e2d2c8f1608f0765db8433601296 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Mar 2026 11:08:45 +0200 Subject: [PATCH 14/26] count_for_image_transformations_bucket_ --- src/Appwrite/Platform/Workers/StatsResources.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 858370c421..5048e6f70d 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -175,7 +175,7 @@ class StatsResources extends Action try { $this->countImageTransformations($dbForProject, $dbForLogs, $region); } catch (Throwable $th) { - call_user_func_array($this->logError, [$th, "StatsResources", "count_for_buckets_{$project->getId()}"]); + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_image_transformations_{$project->getId()}"]); } try { @@ -227,9 +227,15 @@ class StatsResources extends Action $totalImageTransformations = 0; $last30Days = (new \DateTime())->sub(\DateInterval::createFromDateString('30 days'))->format('Y-m-d 00:00:00'); $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $last30Days, $region, &$totalImageTransformations) { - $imageTransformations = $dbForProject->count('bucket_' . $bucket->getSequence(), [ - Query::greaterThanEqual('transformedAt', $last30Days), - ]); + try { + $imageTransformations = $dbForProject->count('bucket_' . $bucket->getSequence(), [ + Query::greaterThanEqual('transformedAt', $last30Days), + ]); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_image_transformations_bucket_{$bucket->getSequence()}"]); + return; + } + $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); $this->createStatsDocuments($region, $metric, $imageTransformations); $totalImageTransformations += $imageTransformations; From e0269e268f2475bf9dc274e370ccc76d21357d8d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 5 Mar 2026 15:31:37 +0530 Subject: [PATCH 15/26] fix: re-read project from DB before updating accessedAt to prevent stale writes Stale in-memory project documents in ScheduleBase (and request-scoped copies in api.php/general.php) were overwriting current DB state when updateProjectAccess triggered. Because Database::updateDocument uses array_merge with the passed document taking priority, cached projects missing recent OAuth provider changes would silently disable them. Now fetches a fresh project document from the DB before writing, so only accessedAt is updated without clobbering other fields. --- app/controllers/general.php | 5 +++-- app/controllers/shared/api.php | 5 +++-- src/Appwrite/Platform/Tasks/ScheduleBase.php | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 43c7e47ca6..207d18481e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -132,8 +132,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => DateTime::now() + ]))); } /** diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index d7b2c7339e..5969a8d1da 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -360,8 +360,9 @@ Http::init() if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => DateTime::now() + ]))); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index cfcdc2503e..cff94bfbea 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,8 +59,9 @@ abstract class ScheduleBase extends Action if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => DateTime::now() + ])); } } } From ab859422765e809ea01b3573b5470ac74487429e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 5 Mar 2026 23:19:54 +1300 Subject: [PATCH 16/26] fix: add retry and session verification to getRoot() for parallel tests getRoot() now retries up to 5 times with session verification to handle race conditions when multiple paratest workers initialize simultaneously. Previously, if account creation or session creation failed under load, all subsequent test requests would fail with 401. Co-Authored-By: Claude Opus 4.6 --- tests/e2e/Scopes/Scope.php | 86 +++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index b0203cbf40..5c34dd4941 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -432,41 +432,67 @@ abstract class Scope extends TestCase return self::$root; } - // Use more entropy to avoid collisions in parallel test execution - $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; - $password = 'password'; - $name = 'User Name'; + $maxRetries = 5; - $root = $this->client->call(Client::METHOD_POST, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); + for ($attempt = 0; $attempt < $maxRetries; $attempt++) { + // Use more entropy to avoid collisions in parallel test execution + $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; + $password = 'password'; + $name = 'User Name'; - $this->assertEquals(201, $root['headers']['status-code']); + $root = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); - $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'email' => $email, - 'password' => $password, - ]); + if ($root['headers']['status-code'] !== 201) { + \usleep(500000); + continue; + } - self::$root = [ - '$id' => ID::custom($root['body']['$id']), - 'name' => $root['body']['name'], - 'email' => $root['body']['email'], - 'session' => $session['cookies']['a_session_console'], - ]; + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'email' => $email, + 'password' => $password, + ]); - return self::$root; + if (empty($session['cookies']['a_session_console'])) { + \usleep(500000); + continue; + } + + // Verify session is valid before returning + $verify = $this->client->call(Client::METHOD_GET, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $session['cookies']['a_session_console'], + 'x-appwrite-project' => 'console', + ]); + + if ($verify['headers']['status-code'] === 200) { + self::$root = [ + '$id' => ID::custom($root['body']['$id']), + 'name' => $root['body']['name'], + 'email' => $root['body']['email'], + 'session' => $session['cookies']['a_session_console'], + ]; + + return self::$root; + } + + \usleep(500000); + } + + $this->fail('Failed to create and verify root session after ' . $maxRetries . ' attempts'); } /** From ee107d30b3d8044aeb83a1176d216db2758934ae Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 6 Mar 2026 01:19:18 +1300 Subject: [PATCH 17/26] fix: make Projects tests parallel-safe - Use getLastEmailByAddress for SMTP tests instead of getLastEmail(2) to avoid shared mail server state issues under parallel execution - Add retry logic to setupProject, setupProjectData, and setupScheduleProjectData for intermittent 401 errors Co-Authored-By: Claude Opus 4.6 --- tests/e2e/Services/Projects/ProjectsBase.php | 96 ++++++++++++------- .../Projects/ProjectsConsoleClientTest.php | 34 +++---- .../Projects/Schedules/SchedulesBase.php | 51 ++++++---- 3 files changed, 109 insertions(+), 72 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php index e8dac843b4..dc31b7aa85 100644 --- a/tests/e2e/Services/Projects/ProjectsBase.php +++ b/tests/e2e/Services/Projects/ProjectsBase.php @@ -26,26 +26,39 @@ trait ProjectsBase return self::$cachedProjectData; } - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => 'Project Test', - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - - $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'projectId' => ID::unique(), - 'name' => 'Project Test', - 'teamId' => $team['body']['$id'], - 'region' => System::getEnv('_APP_REGION', 'default') - ]); + $teamId = ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'Project Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409]); + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Project Test', + 'teamId' => $team['body']['$id'] ?? $teamId, + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } $this->assertEquals(201, $project['headers']['status-code']); self::$cachedProjectData = [ @@ -396,27 +409,42 @@ trait ProjectsBase protected function setupProject(mixed $params, ?string $teamId = null, bool $newTeam = true): string { if ($newTeam) { - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + $generatedTeamId = $teamId ?? ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $generatedTeamId, + 'name' => 'Project Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + + $this->assertContains($team['headers']['status-code'], [201, 409], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT)); + + $teamId = $team['body']['$id'] ?? $generatedTeamId; + } + + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'teamId' => $teamId ?? ID::unique(), - 'name' => 'Project Test', + ...$params, + 'teamId' => $teamId, ]); - - $this->assertEquals(201, $team['headers']['status-code'], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT)); - - $teamId = $team['body']['$id']; + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); } - $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - ...$params, - 'teamId' => $teamId, - ]); - $this->assertEquals(201, $project['headers']['status-code'], 'Setup project failed with status code: ' . $project['headers']['status-code'] . ' and response: ' . json_encode($project['body'], JSON_PRETTY_PRINT)); return $project['body']['$id']; diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index f5937bccfa..50d8a5287b 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -951,26 +951,22 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); - $emails = $this->getLastEmail(2); - $this->assertCount(2, $emails); - $this->assertEquals('custommailer@appwrite.io', $emails[0]['from'][0]['address']); - $this->assertEquals('Custom Mailer', $emails[0]['from'][0]['name']); - $this->assertEquals('reply@appwrite.io', $emails[0]['replyTo'][0]['address']); - $this->assertEquals('Custom Mailer', $emails[0]['replyTo'][0]['name']); - $this->assertEquals('Custom SMTP email sample', $emails[0]['subject']); - $this->assertStringContainsStringIgnoringCase('working correctly', $emails[0]['text']); - $this->assertStringContainsStringIgnoringCase('working correctly', $emails[0]['html']); - $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $emails[0]['text']); - $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $emails[0]['html']); + $smtpProbe = function ($email) { + $this->assertEquals('Custom SMTP email sample', $email['subject']); + }; + $email1 = $this->getLastEmailByAddress('testuser@appwrite.io', $smtpProbe); + $email2 = $this->getLastEmailByAddress('testusertwo@appwrite.io', $smtpProbe); - $to = [ - $emails[0]['to'][0]['address'], - $emails[1]['to'][0]['address'] - ]; - \sort($to); - - $this->assertEquals('testuser@appwrite.io', $to[0]); - $this->assertEquals('testusertwo@appwrite.io', $to[1]); + $this->assertEquals('custommailer@appwrite.io', $email1['from'][0]['address']); + $this->assertEquals('Custom Mailer', $email1['from'][0]['name']); + $this->assertEquals('reply@appwrite.io', $email1['replyTo'][0]['address']); + $this->assertEquals('Custom Mailer', $email1['replyTo'][0]['name']); + $this->assertEquals('Custom SMTP email sample', $email1['subject']); + $this->assertStringContainsStringIgnoringCase('working correctly', $email1['text']); + $this->assertStringContainsStringIgnoringCase('working correctly', $email1['html']); + $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $email1['text']); + $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $email1['html']); + $this->assertEquals('custommailer@appwrite.io', $email2['from'][0]['address']); $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/smtp/tests', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Projects/Schedules/SchedulesBase.php b/tests/e2e/Services/Projects/Schedules/SchedulesBase.php index cd3be80149..681e39b662 100644 --- a/tests/e2e/Services/Projects/Schedules/SchedulesBase.php +++ b/tests/e2e/Services/Projects/Schedules/SchedulesBase.php @@ -16,26 +16,39 @@ trait SchedulesBase return self::$cachedScheduleProjectData; } - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => 'Schedule Test Team', - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - - $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'projectId' => ID::unique(), - 'name' => 'Schedule Test Project', - 'teamId' => $team['body']['$id'], - 'region' => System::getEnv('_APP_REGION', 'default'), - ]); + $teamId = ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'Schedule Test Team', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409]); + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Schedule Test Project', + 'teamId' => $team['body']['$id'] ?? $teamId, + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } $this->assertEquals(201, $project['headers']['status-code']); $projectId = $project['body']['$id']; From 65780d75f991652a2cd4d8e06428b581578d244d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 6 Mar 2026 03:01:23 +1300 Subject: [PATCH 18/26] =?UTF-8?q?fix:=20test=20flakes=20=E2=80=94=20correc?= =?UTF-8?q?t=20index=20length=20fallback,=20add=20retries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix getMaxIndexLength() fallback from 768 to 767 to match MariaDB's actual InnoDB index key limit - Add retry logic to getConsoleVariables() to handle intermittent 401s - Add retry logic to API key creation in ProjectCustom to prevent cascading 401 failures in test methods Co-Authored-By: Claude Opus 4.6 --- tests/e2e/Scopes/ProjectCustom.php | 132 ++++++++++++++++------------- tests/e2e/Scopes/Scope.php | 23 +++-- 2 files changed, 88 insertions(+), 67 deletions(-) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index fa317a2b48..c7e3a520b6 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -102,67 +102,79 @@ trait ProjectCustom $this->assertEquals(201, $project['headers']['status-code'], 'Project creation failed with status: ' . $project['headers']['status-code']); $this->assertNotEmpty($project['body']); - $key = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/keys', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'keyId' => ID::unique(), - 'name' => 'Demo Project Key', - 'scopes' => [ - 'users.read', - 'users.write', - 'teams.read', - 'teams.write', - 'databases.read', - 'databases.write', - 'collections.read', - 'collections.write', - 'tables.read', - 'tables.write', - 'documents.read', - 'documents.write', - 'rows.read', - 'rows.write', - 'files.read', - 'files.write', - 'buckets.read', - 'buckets.write', - 'sites.read', - 'sites.write', - 'functions.read', - 'functions.write', - 'sites.read', - 'sites.write', - 'execution.read', - 'execution.write', - 'log.read', - 'log.write', - 'locale.read', - 'avatars.read', - 'health.read', - 'rules.read', - 'rules.write', - 'sessions.write', - 'targets.read', - 'targets.write', - 'providers.read', - 'providers.write', - 'messages.read', - 'messages.write', - 'topics.write', - 'topics.read', - 'subscribers.write', - 'subscribers.read', - 'migrations.write', - 'migrations.read', - 'tokens.read', - 'tokens.write', - ], - ]); + $key = null; + for ($i = 0; $i < $maxRetries; $i++) { + $key = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/keys', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'keyId' => ID::unique(), + 'name' => 'Demo Project Key', + 'scopes' => [ + 'users.read', + 'users.write', + 'teams.read', + 'teams.write', + 'databases.read', + 'databases.write', + 'collections.read', + 'collections.write', + 'tables.read', + 'tables.write', + 'documents.read', + 'documents.write', + 'rows.read', + 'rows.write', + 'files.read', + 'files.write', + 'buckets.read', + 'buckets.write', + 'sites.read', + 'sites.write', + 'functions.read', + 'functions.write', + 'sites.read', + 'sites.write', + 'execution.read', + 'execution.write', + 'log.read', + 'log.write', + 'locale.read', + 'avatars.read', + 'health.read', + 'rules.read', + 'rules.write', + 'sessions.write', + 'targets.read', + 'targets.write', + 'providers.read', + 'providers.write', + 'messages.read', + 'messages.write', + 'topics.write', + 'topics.read', + 'subscribers.write', + 'subscribers.read', + 'migrations.write', + 'migrations.read', + 'tokens.read', + 'tokens.write', + ], + ]); - $this->assertEquals(201, $key['headers']['status-code']); + if ($key['headers']['status-code'] === 201) { + break; + } + + if ($key['headers']['status-code'] === 401 && $i < $maxRetries - 1) { + \usleep(500000); + continue; + } + } + + $this->assertEquals(201, $key['headers']['status-code'], 'Key creation failed with status: ' . $key['headers']['status-code']); $this->assertNotEmpty($key['body']); $this->assertNotEmpty($key['body']['secret']); diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 5c34dd4941..a8152ef77e 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -59,12 +59,21 @@ abstract class Scope extends TestCase $root = $this->getRoot(); - $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - 'cookie' => 'a_session_console=' . $root['session'], - ]); + for ($i = 0; $i < 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $root['session'], + ]); + + if ($response['headers']['status-code'] === 200 && !empty($response['body'])) { + self::$consoleVariables = $response['body']; + return self::$consoleVariables; + } + + \usleep(500000); + } self::$consoleVariables = $response['body'] ?? []; @@ -140,7 +149,7 @@ abstract class Scope extends TestCase */ protected function getMaxIndexLength(): int { - return $this->getConsoleVariables()['maxIndexLength'] ?? 768; + return $this->getConsoleVariables()['maxIndexLength'] ?? 767; } /** From 71dff441edf41a3088604027e9e11782cc83af3f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 5 Mar 2026 20:03:42 +0530 Subject: [PATCH 19/26] fix: Update in-memory project document after accessedAt update Previously, the updateProjectAccess method updated the database with the new accessedAt timestamp but did not update the in-memory project document. This caused the if statement to constantly evaluate to true on subsequent calls, triggering unnecessary database updates. --- src/Appwrite/Platform/Tasks/ScheduleBase.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index cff94bfbea..c55e3d4a6a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,9 +59,11 @@ abstract class ScheduleBase extends Action if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { + $now = DateTime::now(); $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ - 'accessedAt' => DateTime::now() + 'accessedAt' => $now ])); + $project->setAttribute('accessedAt', $now); } } } From 91edf820600bb1eef8e2c91ea0f39d54d105ebfb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 6 Mar 2026 03:41:35 +1300 Subject: [PATCH 20/26] fix: database index length assertion and email race in password recovery - Remove specific index length number from testPatchAttribute assertion since the value differs between shared/non-shared table modes (767 vs 768) and the console API returns the console project's value, not the user project's - Use getLastEmailByAddress in testPasswordRecoveryUrlParams to avoid retrieving emails from parallel test classes sharing the same maildev Co-Authored-By: Claude Opus 4.6 --- .../e2e/Services/Databases/DatabasesBase.php | 3 +-- .../Projects/ProjectsConsoleClientTest.php | 25 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 0c784d717f..7f23f2966c 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -1270,8 +1270,7 @@ trait DatabasesBase ]); $this->assertEquals(400, $attribute['headers']['status-code']); - $maxLength = $this->getMaxIndexLength(); - $this->assertStringContainsString('Index length is longer than the maximum: '.$maxLength, $attribute['body']['message']); + $this->assertStringContainsString('Index length is longer than the maximum:', $attribute['body']['message']); } public function testUpdateAttributeEnum(): void diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 50d8a5287b..52e59f3e72 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -6423,11 +6423,12 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url) { + $userEmail = $this->getUser()['email']; + + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url) { $this->assertStringContainsString($url, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "&userId=" . $userId . "&secret="; @@ -6446,7 +6447,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6456,11 +6457,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url) { $this->assertStringContainsString($url, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "&userId=" . $userId . "&secret="; @@ -6479,7 +6479,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6489,11 +6489,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url, $userId) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url, $userId) { $this->assertStringContainsString($url . '?userId=' . $userId, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "?userId=" . $userId . "&secret="; @@ -6512,7 +6511,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6522,11 +6521,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url, $userId) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url, $userId) { $this->assertStringContainsString($url . '?userId=' . $userId, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "?userId=" . $userId . "&secret="; @@ -6545,7 +6543,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6555,11 +6553,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) { $this->assertStringContainsString('INJECTED', $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $this->assertStringContainsString('INJECTED', $lastEmail['html']); From 8b026d345909e059918d5b0cf8095076373f7806 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 6 Mar 2026 15:12:07 +0530 Subject: [PATCH 21/26] perf: optimize updateDocument() calls to use sparse documents Optimize updateDocument() calls across the codebase to pass only changed attributes as sparse Document objects rather than full documents. This is more efficient because updateDocument() internally performs array_merge(). Changes: - Updated 58 files to use sparse Document objects - Added Performance Patterns section to AGENTS.md with optimization guidelines - Applied pattern to Workers, Functions, Sites, Teams, VCS modules - Updated app/controllers/api files (account, users, messaging) - Updated app infrastructure files (realtime, general, init/resources, shared/api) Exceptions maintained: - Migration files (need full document updates by design) - Cases with 6+ attributes (marginal benefit) - Complex nested relationship logic --- AGENTS.md | 30 ++++++++ CLAUDE.md | 1 + app/controllers/api/account.php | 52 ++++++++++---- app/controllers/api/users.php | 60 +++++++++++----- app/controllers/general.php | 5 +- app/controllers/shared/api.php | 16 +++-- app/init/resources.php | 13 +++- app/realtime.php | 5 +- .../Account/MFA/Authenticators/Update.php | 4 +- .../Http/Account/MFA/Challenges/Update.php | 8 ++- .../Http/Account/MFA/RecoveryCodes/Create.php | 2 +- .../Http/Account/MFA/RecoveryCodes/Update.php | 2 +- .../Account/Http/Account/MFA/Update.php | 4 +- .../Platform/Modules/Compute/Base.php | 14 +++- .../Collections/Attributes/Delete.php | 7 +- .../Databases/Collections/Indexes/Delete.php | 4 +- .../Modules/Databases/Workers/Databases.php | 16 ++++- .../Functions/Http/Deployments/Create.php | 38 ++++++---- .../Functions/Http/Deployments/Delete.php | 11 +-- .../Http/Deployments/Duplicate/Create.php | 8 ++- .../Http/Deployments/Status/Update.php | 6 +- .../Http/Deployments/Template/Create.php | 7 +- .../Functions/Http/Executions/Delete.php | 6 +- .../Functions/Http/Functions/Create.php | 31 ++++---- .../Functions/Http/Functions/Delete.php | 6 +- .../Http/Functions/Deployment/Update.php | 11 ++- .../Functions/Http/Variables/Create.php | 9 ++- .../Functions/Http/Variables/Delete.php | 10 ++- .../Functions/Http/Variables/Update.php | 17 ++++- .../Modules/Functions/Workers/Builds.php | 72 +++++++++++++++---- .../Modules/Projects/Http/DevKeys/Update.php | 3 +- .../Projects/Http/Projects/Labels/Update.php | 5 +- .../Projects/Http/Projects/Team/Update.php | 20 +++--- .../Modules/Sites/Http/Deployments/Create.php | 26 +++++-- .../Modules/Sites/Http/Deployments/Delete.php | 11 +-- .../Http/Deployments/Duplicate/Create.php | 7 +- .../Sites/Http/Deployments/Status/Update.php | 6 +- .../Http/Deployments/Template/Create.php | 7 +- .../Modules/Sites/Http/Sites/Create.php | 7 +- .../Sites/Http/Sites/Deployment/Update.php | 5 +- .../Modules/Sites/Http/Variables/Create.php | 4 +- .../Modules/Sites/Http/Variables/Delete.php | 5 +- .../Modules/Sites/Http/Variables/Update.php | 12 +++- .../Modules/Teams/Http/Memberships/Create.php | 21 +++--- .../Modules/Teams/Http/Memberships/Delete.php | 5 +- .../Teams/Http/Memberships/Status/Update.php | 2 +- .../Modules/Teams/Http/Memberships/Update.php | 2 +- .../Modules/Teams/Http/Teams/Name/Update.php | 3 +- .../Http/GitHub/Authorize/External/Update.php | 3 +- .../Modules/VCS/Http/GitHub/Callback/Get.php | 8 ++- .../Modules/VCS/Http/GitHub/Deployment.php | 7 +- .../Modules/VCS/Http/GitHub/Events/Create.php | 3 +- .../Installations/Repositories/Create.php | 6 +- src/Appwrite/Platform/Tasks/Interval.php | 4 +- .../Platform/Workers/Certificates.php | 6 +- src/Appwrite/Platform/Workers/Messaging.php | 8 ++- src/Appwrite/Platform/Workers/Migrations.php | 9 ++- src/Appwrite/Platform/Workers/Webhooks.php | 10 ++- 58 files changed, 505 insertions(+), 185 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 993b0b5ad0..bb24d9f4fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,36 @@ Examples: 'resourceType' => 'deployments' ``` +## Performance Patterns + +### Document Update Optimization + +When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`. + +**Correct Pattern:** +```php +// Good: Pass only changed attributes directly +$user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'name' => $name, + 'email' => $email, +])); +``` + +**Incorrect Pattern:** +```php +$user->setAttribute('name', $name); +$user->setAttribute('email', $email); + +// Bad: Passing full document is inefficient +$user = $dbForProject->updateDocument('users', $user->getId(), $user); +``` + +**Exceptions:** +- Migration files (need full document updates by design) +- Cases already using `array_merge()` with `getArrayCopy()` +- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document) +- Complex nested relationship logic where full document state is required + ## Security Considerations ### Critical Security Practices diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 0adf1b2d57..b58a9b4185 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -288,7 +288,10 @@ $createSession = function (string $userId, string $secret, Request $request, Res } try { - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'emailVerification' => $user->getAttribute('emailVerification'), + 'phoneVerification' => $user->getAttribute('phoneVerification'), + ])); } catch (\Throwable $th) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); } @@ -1032,7 +1035,11 @@ Http::post('/v1/account/sessions/email') ->setAttribute('password', $proofForPasswordUpdated->hash($password)) ->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName()) ->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions()); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'hash' => $user->getAttribute('hash'), + 'hashOptions' => $user->getAttribute('hashOptions'), + ])); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -1822,7 +1829,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setAttribute('providerAccessToken', $accessToken) ->setAttribute('providerRefreshToken', $refreshToken) ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry)); - $dbForProject->updateDocument('identities', $identity->getId(), $identity); + $dbForProject->updateDocument('identities', $identity->getId(), new Document([ + 'providerAccessToken' => $identity->getAttribute('providerAccessToken'), + 'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'), + 'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'), + ])); } if (empty($user->getAttribute('email'))) { @@ -1960,7 +1971,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setAttribute('sessionId', $session->getId()) ->setAttribute('sessionInternalId', $session->getSequence()); - $dbForProject->updateDocument('targets', $target->getId(), $target); + $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'sessionId' => $target->getAttribute('sessionId'), + 'sessionInternalId' => $target->getAttribute('sessionInternalId'), + ])); } } @@ -3145,7 +3159,9 @@ Http::patch('/v1/account/name') $user->setAttribute('name', $name); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'name' => $user->getAttribute('name'), + ])); $queueForEvents->setParam('userId', $user->getId()); @@ -3798,13 +3814,15 @@ Http::put('/v1/account/recovery') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile - ->setAttribute('password', $newPassword) - ->setAttribute('passwordHistory', $history) - ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', $proofForPassword->getHash()->getName()) - ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()) - ->setAttribute('emailVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document( + [ + 'password' => $newPassword, + 'passwordHistory' => $history, + 'passwordUpdate' => DateTime::now(), + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), + 'emailVerification' => true] + )); $user->setAttributes($profile->getArrayCopy()); @@ -4126,7 +4144,7 @@ Http::put('/v1/account/verifications/email') $authorization->addRole(Role::user($profile->getId())->toString()); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true])); $user->setAttributes($profile->getArrayCopy()); @@ -4342,7 +4360,7 @@ Http::put('/v1/account/verifications/phone') $authorization->addRole(Role::user($profile->getId())->toString()); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true])); $user->setAttributes($profile->getArrayCopy()); @@ -4500,7 +4518,11 @@ Http::put('/v1/account/targets/:targetId/push') $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}"); - $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $target = $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'identifier' => $target->getAttribute('identifier'), + 'expired' => $target->getAttribute('expired'), + 'name' => $target->getAttribute('name'), + ])); $dbForProject->purgeCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 38c0e87b44..d0e5e19a51 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1161,7 +1161,7 @@ Http::patch('/v1/users/:userId/status') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1204,7 +1204,7 @@ Http::put('/v1/users/:userId/labels') $user->setAttribute('labels', (array) \array_values(\array_unique($labels))); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1245,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1289,7 +1289,7 @@ Http::patch('/v1/users/:userId/name') $user->setAttribute('name', $name); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')])); $queueForEvents->setParam('userId', $user->getId()); @@ -1344,7 +1344,10 @@ Http::patch('/v1/users/:userId/password') ->setAttribute('password', '') ->setAttribute('passwordUpdate', DateTime::now()); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'passwordUpdate' => $user->getAttribute('passwordUpdate'), + ])); $queueForEvents->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); } @@ -1377,7 +1380,13 @@ Http::patch('/v1/users/:userId/password') ->setAttribute('hash', $hasher->getName()) ->setAttribute('hashOptions', $hasher->getOptions()); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'passwordHistory' => $user->getAttribute('passwordHistory'), + 'passwordUpdate' => $user->getAttribute('passwordUpdate'), + 'hash' => $user->getAttribute('hash'), + 'hashOptions' => $user->getAttribute('hashOptions'), + ])); $sessions = $user->getAttribute('sessions', []); $invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false; @@ -1469,7 +1478,15 @@ Http::patch('/v1/users/:userId/email') ; try { - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'email' => $user->getAttribute('email'), + 'emailVerification' => $user->getAttribute('emailVerification'), + 'emailCanonical' => $user->getAttribute('emailCanonical'), + 'emailIsCanonical' => $user->getAttribute('emailIsCanonical'), + 'emailIsCorporate' => $user->getAttribute('emailIsCorporate'), + 'emailIsDisposable' => $user->getAttribute('emailIsDisposable'), + 'emailIsFree' => $user->getAttribute('emailIsFree'), + ])); /** * @var Document $oldTarget */ @@ -1477,7 +1494,8 @@ Http::patch('/v1/users/:userId/email') if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { if (\strlen($email) !== 0) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); + $dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email])); + $oldTarget->setAttribute('identifier', $email); } else { $dbForProject->deleteDocument('targets', $oldTarget->getId()); } @@ -1558,7 +1576,10 @@ Http::patch('/v1/users/:userId/phone') } try { - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'phone' => $user->getAttribute('phone'), + 'phoneVerification' => $user->getAttribute('phoneVerification'), + ])); /** * @var Document $oldTarget */ @@ -1566,7 +1587,8 @@ Http::patch('/v1/users/:userId/phone') if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { if (\strlen($number) !== 0) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number)); + $dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number])); + $oldTarget->setAttribute('identifier', $number); } else { $dbForProject->deleteDocument('targets', $oldTarget->getId()); } @@ -1630,7 +1652,7 @@ Http::patch('/v1/users/:userId/verification') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification])); $queueForEvents->setParam('userId', $user->getId()); @@ -1668,7 +1690,7 @@ Http::patch('/v1/users/:userId/prefs') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1768,7 +1790,13 @@ Http::patch('/v1/users/:userId/targets/:targetId') $target->setAttribute('name', $name); } - $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $target = $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'identifier' => $target->getAttribute('identifier'), + 'expired' => $target->getAttribute('expired'), + 'providerId' => $target->getAttribute('providerId'), + 'providerInternalId' => $target->getAttribute('providerInternalId'), + 'name' => $target->getAttribute('name'), + ])); $dbForProject->purgeCachedDocument('users', $user->getId()); $queueForEvents @@ -1836,7 +1864,7 @@ Http::patch('/v1/users/:userId/mfa') $user->setAttribute('mfa', $mfa); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')])); $queueForEvents->setParam('userId', $user->getId()); @@ -2024,7 +2052,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes') $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); @@ -2096,7 +2124,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes') $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/app/controllers/general.php b/app/controllers/general.php index 207d18481e..a4618fe982 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1650,7 +1650,10 @@ Http::get('/v1/ping') ->setAttribute('pingedAt', $pingedAt); $authorization->skip(function () use ($dbForPlatform, $project) { - $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'pingCount' => $project->getAttribute('pingCount'), + 'pingedAt' => $project->getAttribute('pingedAt') + ])); }); $queueForEvents diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5969a8d1da..d2abfe53a2 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -372,9 +372,13 @@ Http::init() $user->setAttribute('accessedAt', DateTime::now()); if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) { - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'accessedAt' => $user->getAttribute('accessedAt') + ])); } else { - $authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([ + 'accessedAt' => $user->getAttribute('accessedAt') + ]))); } } } @@ -650,7 +654,9 @@ Http::init() $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); + $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([ + 'transformedAt' => $file->getAttribute('transformedAt') + ]))); } } } @@ -949,7 +955,9 @@ Http::shutdown() } } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); - $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([ + 'accessedAt' => $cacheLog->getAttribute('accessedAt') + ]))); // Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load() $cache->save($key, $data['payload']); } diff --git a/app/init/resources.php b/app/init/resources.php index 36aadc9707..d5486c2a49 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1235,7 +1235,9 @@ Http::setResource('devKey', function (Request $request, Document $project, array $accessedAt = $key->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } @@ -1252,7 +1254,10 @@ Http::setResource('devKey', function (Request $request, Document $project, array /** Update access time as well */ $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'sdks' => $key->getAttribute('sdks'), + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } } @@ -1409,7 +1414,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request, $accessedAt = $token->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { $token->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token)); + $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ + 'accessedAt' => $token->getAttribute('accessedAt') + ]))); } return new Document([ diff --git a/app/realtime.php b/app/realtime.php index 7ec24d03c8..e0591a2596 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -356,7 +356,10 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - $database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + $database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([ + 'timestamp' => $statsDocument->getAttribute('timestamp'), + 'value' => $statsDocument->getAttribute('value') + ]))); } catch (Throwable $th) { logError($th, "updateWorkerDocument"); } diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php index bd961ffbc2..f7226d915e 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php @@ -119,7 +119,7 @@ class Update extends Action $authenticator->setAttribute('verified', true); - $dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator); + $dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true])); $dbForProject->purgeCachedDocument('users', $user->getId()); $factors = $session->getAttribute('factors', []); @@ -127,7 +127,7 @@ class Update extends Action $factors = \array_values(\array_unique($factors)); $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php index 3f3532cf16..5f6b7a1186 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php @@ -117,7 +117,7 @@ class Update extends Action $mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]); $mfaRecoveryCodes = \array_values($mfaRecoveryCodes); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); return true; } @@ -147,11 +147,13 @@ class Update extends Action $factors[] = $type; $factors = \array_values(\array_unique($factors)); + $mfaUpdatedAt = DateTime::now(); + $session ->setAttribute('factors', $factors) - ->setAttribute('mfaUpdatedAt', DateTime::now()); + ->setAttribute('mfaUpdatedAt', $mfaUpdatedAt); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt])); $queueForEvents ->setParam('userId', $user->getId()) diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php index 969cf9c262..0d1191aaea 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php @@ -93,7 +93,7 @@ class Create extends Action $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php index 9b9f9b7e00..40051cfebc 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php @@ -91,7 +91,7 @@ class Update extends Action $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php index 227062caa3..a578149654 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php @@ -72,7 +72,7 @@ class Update extends Action ): void { $user->setAttribute('mfa', $mfa); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa])); if ($mfa) { $factors = $session->getAttribute('factors', []); @@ -89,7 +89,7 @@ class Update extends Action $factors = \array_values(\array_unique($factors)); $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors])); } $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 08b4bbced8..f388e46f83 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -143,7 +143,12 @@ class Base extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) @@ -249,7 +254,12 @@ class Base extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index f860fc68af..38b96e67bc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator; use Utopia\Database\Validator\Key; @@ -98,7 +99,8 @@ class Delete extends Action } if ($attribute->getAttribute('status') === 'available') { - $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting')); + $attribute->setAttribute('status', 'deleting'); + $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); @@ -118,7 +120,8 @@ class Delete extends Action } if ($relatedAttribute->getAttribute('status') === 'available') { - $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting')); + $relatedAttribute->setAttribute('status', 'deleting'); + $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index e1c8ad928d..dea62bfc16 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -96,7 +97,8 @@ class Delete extends Action // Only update status if removing available index if ($index->getAttribute('status') === 'available') { - $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); + $index->setAttribute('status', 'deleting'); + $index = $dbForProject->updateDocument('indexes', $index->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 9d686f72ed..60d70b7942 100644 --- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -322,13 +322,19 @@ class Databases extends Action $dbForProject->updateDocument( 'attributes', $attribute->getId(), - $attribute->setAttribute('status', 'stuck') + new Document([ + 'error' => $attribute->getAttribute('error'), + 'status' => 'stuck', + ]) ); if (!$relatedAttribute->isEmpty()) { $dbForProject->updateDocument( 'attributes', $relatedAttribute->getId(), - $relatedAttribute->setAttribute('status', 'stuck') + new Document([ + 'error' => $relatedAttribute->getAttribute('error'), + 'status' => 'stuck', + ]) ); } @@ -382,7 +388,11 @@ class Databases extends Action if ($exists) { // Delete the duplicate if created, else update in db $this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime); } else { - $dbForProject->updateDocument('indexes', $index->getId(), $index); + $dbForProject->updateDocument('indexes', $index->getId(), new Document([ + 'attributes' => $index->getAttribute('attributes'), + 'lengths' => $index->getAttribute('lengths'), + 'orders' => $index->getAttribute('orders'), + ])); } } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index ddb1f486d7..65b6ffd5bb 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -227,7 +227,9 @@ class Create extends Action foreach ($activeDeployments as $activeDeployment) { $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([ + 'activate' => false, + ])); } } @@ -255,14 +257,17 @@ class Create extends Action 'type' => $type ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceMetadata' => $metadata, + ])); } // Start the build @@ -295,14 +300,17 @@ class Create extends Action 'type' => $type ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php index 5cab00c0fa..3d75919eb8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -107,11 +107,12 @@ class Delete extends Action $function = $dbForProject->updateDocument( 'functions', $function->getId(), - $function - ->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt()) - ->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence()) - ->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId()) - ->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', '')) + new Document([ + 'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(), + 'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(), + 'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(), + 'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''), + ]) ); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php index 2769d3dc1e..9884b12dba 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -119,7 +120,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $function->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php index c3cd93830a..dab477ef1f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php @@ -91,7 +91,7 @@ class Update extends Action $endTime = new \DateTime('now'); $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([ + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'buildEndedAt' => DateTime::now(), 'buildDuration' => $duration, 'status' => 'canceled' @@ -99,7 +99,9 @@ class Update extends Action if ($deployment->getSequence() === $function->getAttribute('latestDeploymentInternalId', '')) { $function = $function->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); } try { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 28226cc98a..53af82e701 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -174,7 +174,12 @@ class Create extends Base ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $function->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); $this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php index 4a04afa119..21ec3c66ce 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -119,7 +120,10 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'active' => $schedule->getAttribute('active'), + ]))); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 4cabccccee..8d657c1064 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -283,7 +283,12 @@ class Create extends Base $function->setAttribute('repositoryInternalId', $repository->getSequence()); } - $function = $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'scheduleId' => $function->getAttribute('scheduleId'), + 'scheduleInternalId' => $function->getAttribute('scheduleInternalId'), + 'repositoryId' => $function->getAttribute('repositoryId'), + 'repositoryInternalId' => $function->getAttribute('repositoryInternalId'), + ])); // Backwards compatibility with 1.6 behaviour $requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); @@ -321,12 +326,12 @@ class Create extends Base referenceType: 'branch' ); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } elseif (!$template->isEmpty()) { // Deploy non-VCS from template $deploymentId = ID::unique(); @@ -347,12 +352,12 @@ class Create extends Base 'activate' => true, ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php index 1d108c425f..fb45cee82f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -90,7 +91,10 @@ class Delete extends Base $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'active' => $schedule->getAttribute('active'), + ]))); } $queueForDeletes diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php index e78b17bd3f..6b6eda36ab 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php @@ -103,7 +103,11 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $queries = [ Query::equal('trigger', ['manual']), @@ -119,7 +123,10 @@ class Update extends Base ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'deploymentId' => $rule->getAttribute('deploymentId'), + 'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'), + ]))); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index fa5ca39e7e..fee5b0095d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -105,7 +105,8 @@ class Create extends Base throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -113,7 +114,11 @@ class Create extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response ->setStatusCode(Response::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php index c447bef81d..5648596826 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -86,7 +87,8 @@ class Delete extends Base $dbForProject->deleteDocument('variables', $variable->getId()); - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -94,7 +96,11 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php index 9c57a3b9c7..acb066ca9c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -99,12 +100,18 @@ class Update extends Base ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); + $dbForProject->updateDocument('variables', $variable->getId(), new Document([ + 'key' => $key, + 'value' => $value ?? $variable->getAttribute('value'), + 'secret' => $secret ?? $variable->getAttribute('secret'), + 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), + ])); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -112,7 +119,11 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response->dynamic($variable, Response::MODEL_VARIABLE); } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 09a70bb71d..7f55d223f6 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -279,7 +279,10 @@ class Builds extends Action $deployment->setAttribute('buildStartedAt', $startTime); $deployment->setAttribute('status', 'processing'); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildStartedAt' => $startTime, + 'status' => 'processing', + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -366,7 +369,11 @@ class Builds extends Action ->setAttribute('sourcePath', $source) ->setAttribute('sourceSize', $directorySize) ->setAttribute('totalSize', $directorySize); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'sourcePath' => $deployment->getAttribute('sourcePath'), + 'sourceSize' => $deployment->getAttribute('sourceSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -480,7 +487,13 @@ class Builds extends Action $deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME); $deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function"); $deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash"); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'providerCommitHash' => $deployment->getAttribute('providerCommitHash'), + 'providerCommitAuthorUrl' => $deployment->getAttribute('providerCommitAuthorUrl'), + 'providerCommitAuthor' => $deployment->getAttribute('providerCommitAuthor'), + 'providerCommitMessage' => $deployment->getAttribute('providerCommitMessage'), + 'providerCommitUrl' => $deployment->getAttribute('providerCommitUrl'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -528,7 +541,11 @@ class Builds extends Action ->setAttribute('sourcePath', $source) ->setAttribute('sourceSize', $directorySize) ->setAttribute('totalSize', $directorySize); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'sourcePath' => $deployment->getAttribute('sourcePath'), + 'sourceSize' => $deployment->getAttribute('sourceSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -543,7 +560,9 @@ class Builds extends Action /** Request the executor to build the code... */ $deployment->setAttribute('status', 'building'); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'status' => 'building', + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -819,7 +838,9 @@ class Builds extends Action if ($affected) { $deployment = $deployment->setAttribute('buildLogs', $currentLogs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $currentLogs, + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -905,7 +926,14 @@ class Builds extends Action } } - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildPath' => $deployment->getAttribute('buildPath'), + 'buildSize' => $deployment->getAttribute('buildSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + 'buildLogs' => $deployment->getAttribute('buildLogs'), + 'adapter' => $deployment->getAttribute('adapter'), + 'fallbackFile' => $deployment->getAttribute('fallbackFile'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); @@ -916,12 +944,15 @@ class Builds extends Action $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Deployment finished. \n"; + $logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[32m Deployment finished. \033[0m\n"; $deployment->setAttribute('buildLogs', $logs); /** Update the status */ $deployment->setAttribute('status', 'ready'); - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + 'status' => 'ready', + ])); Console::log('Status marked as ready'); @@ -1109,7 +1140,11 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $resource->getAttribute('schedule')) ->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deploymentId'))); - $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule); + $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ])); } /** Screenshot site */ @@ -1160,7 +1195,12 @@ class Builds extends Action $deployment->setAttribute('status', 'failed'); $deployment->setAttribute('buildLogs', $message); - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'buildEndedAt' => $deployment->getAttribute('buildEndedAt'), + 'buildDuration' => $deployment->getAttribute('buildDuration'), + 'status' => 'failed', + 'buildLogs' => $message, + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -1467,7 +1507,9 @@ class Builds extends Action $logs .= "[$date] [appwrite] Git action failed. Deployment will continue. \n"; $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -1483,10 +1525,12 @@ class Builds extends Action $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Build has been canceled. \n"; + $logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[33m Build has been canceled. \033[0m\n"; $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php index 925416d5d7..f3e47f80ba 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -73,7 +74,7 @@ class Update extends Action ->setAttribute('name', $name) ->setAttribute('expire', $expire); - $dbForPlatform->updateDocument('devKeys', $key->getId(), $key); + $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document(['name' => $name, 'expire' => $expire])); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php index 1a06c1ee84..de11bb0091 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator; @@ -77,9 +78,9 @@ class Update extends Action throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project->setAttribute('labels', (array) \array_values(\array_unique($labels))); + $labels = (array) \array_values(\array_unique($labels)); - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])); $response->dynamic($project, Response::MODEL_PROJECT); } diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php index df5b2b6245..ab92d1a15f 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; @@ -72,34 +73,31 @@ class Update extends Action $permissions = $this->getPermissions($teamId, $projectId); - $project - ->setAttribute('teamId', $teamId) - ->setAttribute('teamInternalId', $team->getSequence()) - ->setAttribute('$permissions', $permissions); - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'teamId' => $teamId, + 'teamInternalId' => $team->getSequence(), + '$permissions' => $permissions, + ])); $installations = $dbForPlatform->find('installations', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($installations as $installation) { - $installation->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $dbForPlatform->updateDocument('installations', $installation->getId(), new Document(['$permissions' => $permissions])); } $repositories = $dbForPlatform->find('repositories', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($repositories as $repository) { - $repository->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository); + $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['$permissions' => $permissions])); } $vcsComments = $dbForPlatform->find('vcsComments', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($vcsComments as $vcsComment) { - $vcsComment->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment); + $dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), new Document(['$permissions' => $permissions])); } $response->dynamic($project, Response::MODEL_PROJECT); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 0234073882..8a6964209f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -237,7 +237,7 @@ class Create extends Action foreach ($activeDeployments as $activeDeployment) { $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false])); } } @@ -272,7 +272,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; @@ -302,7 +307,10 @@ class Create extends Action ])) ); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceMetadata' => $metadata, + ])); } // Start the build @@ -342,7 +350,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; @@ -368,7 +381,10 @@ class Create extends Action ])) ); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index 7339c510b5..efea79395f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -107,11 +107,12 @@ class Delete extends Action $site = $dbForProject->updateDocument( 'sites', $site->getId(), - $site - ->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt()) - ->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence()) - ->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId()) - ->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', '')) + new Document([ + 'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(), + 'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(), + 'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(), + 'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''), + ]) ); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index d5918f5f12..546549604b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -142,7 +142,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); // Preview deployments for sites $sitesDomain = $platform['sitesDomain']; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php index 45c842bcd0..7ea5cc87cb 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php @@ -89,7 +89,7 @@ class Update extends Action $endTime = new \DateTime('now'); $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([ + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'buildEndedAt' => DateTime::now(), 'buildDuration' => $duration, 'status' => 'canceled' @@ -97,7 +97,9 @@ class Update extends Action if ($deployment->getSequence() === $site->getAttribute('latestDeploymentInternalId', '')) { $site = $site->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); } try { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 3b79bedbe7..f648c57a83 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -187,7 +187,12 @@ class Create extends Base ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 1da9196980..b6896ff505 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -193,9 +193,12 @@ class Create extends Base $repository = $dbForPlatform->createDocument('repositories', $repository); $site->setAttribute('repositoryId', $repository->getId()); $site->setAttribute('repositoryInternalId', $repository->getSequence()); - } - $site = $dbForProject->updateDocument('sites', $site->getId(), $site); + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'repositoryId' => $repository->getId(), + 'repositoryInternalId' => $repository->getSequence(), + ])); + } $queueForEvents->setParam('siteId', $site->getId()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php index 630b9e7c5f..18f80ca53f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php @@ -111,7 +111,10 @@ class Update extends Base ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'deploymentId' => $rule->getAttribute('deploymentId'), + 'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'), + ]))); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 4a3254ee23..04b30fbc9c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -92,7 +92,9 @@ class Create extends Base throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response ->setStatusCode(Response::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index 3e7a2642e6..703806f1aa 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -76,7 +77,9 @@ class Delete extends Base $dbForProject->deleteDocument('variables', $variable->getId()); - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index 4b16ba17a4..99f68a45df 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -93,12 +94,19 @@ class Update extends Base ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); + $dbForProject->updateDocument('variables', $variable->getId(), new Document([ + 'key' => $variable->getAttribute('key'), + 'value' => $variable->getAttribute('value'), + 'secret' => $variable->getAttribute('secret'), + 'search' => $variable->getAttribute('search'), + ])); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response->dynamic($variable, Response::MODEL_VARIABLE); } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 282d78ea0c..3bf597eaca 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -266,17 +266,22 @@ class Create extends Action $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } } elseif ($membership->getAttribute('confirm') === false) { - $membership->setAttribute('secret', $proofForToken->hash($secret)); - $membership->setAttribute('invited', DateTime::now()); + $secretHash = $proofForToken->hash($secret); + $invitedTime = DateTime::now(); if ($isPrivilegedUser || $isAppUser) { - $membership->setAttribute('joined', DateTime::now()); - $membership->setAttribute('confirm', true); + $membership = $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), new Document([ + 'secret' => $secretHash, + 'invited' => $invitedTime, + 'joined' => DateTime::now(), + 'confirm' => true + ]))); + } else { + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document([ + 'secret' => $secretHash, + 'invited' => $invitedTime + ])); } - - $membership = ($isPrivilegedUser || $isAppUser) ? - $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : - $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } else { throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED); } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php index 56f5cd8cb5..3b516c2d60 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php @@ -131,7 +131,10 @@ class Delete extends Action if (!$membership->isEmpty()) { $team->setAttribute('userId', $membership->getAttribute('userId')); $team->setAttribute('userInternalId', $membership->getAttribute('userInternalId')); - $dbForProject->updateDocument('teams', $team->getId(), $team); + $dbForProject->updateDocument('teams', $team->getId(), new Document([ + 'userId' => $membership->getAttribute('userId'), + 'userInternalId' => $membership->getAttribute('userInternalId'), + ])); } } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index eac516c6fe..46b6c3cacf 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -189,7 +189,7 @@ class Update extends Action ; } - $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['joined' => $membership->getAttribute('joined'), 'confirm' => true])); $dbForProject->purgeCachedDocument('users', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php index 98f342cecd..a935055163 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php @@ -116,7 +116,7 @@ class Update extends Action * Update the roles */ $membership->setAttribute('roles', $roles); - $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['roles' => $roles])); /** * Replace membership on profile diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php index 4b058c58e1..ebe751ee1c 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Text; @@ -67,7 +68,7 @@ class Update extends Action ->setAttribute('name', $name) ->setAttribute('search', implode(' ', [$teamId, $name])); - $team = $dbForProject->updateDocument('teams', $team->getId(), $team); + $team = $dbForProject->updateDocument('teams', $team->getId(), new Document(['name' => $name, 'search' => implode(' ', [$teamId, $name])])); $queueForEvents->setParam('teamId', $team->getId()); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php index 6b521e56d0..4a34ffd36a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -98,9 +98,8 @@ class Update extends Action } $providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId])); - $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds]))); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php index cb45efbf39..914bcaa93e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php @@ -152,7 +152,13 @@ class Get extends Action ->setAttribute('personalRefreshToken', $refreshToken) ->setAttribute('personalAccessToken', $accessToken) ->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry); - $installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $installation = $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([ + 'organization' => $installation->getAttribute('organization'), + 'personal' => $installation->getAttribute('personal'), + 'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'), + 'personalAccessToken' => $installation->getAttribute('personalAccessToken'), + 'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'), + ])); } } else { $error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.'; diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 6d493e2cdb..c9904bb32b 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -326,7 +326,12 @@ trait Deployment ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); + $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document([ + 'latestDeploymentId' => $resource->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $resource->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $resource->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $resource->getAttribute('latestDeploymentStatus'), + ]))); if ($resource->getCollection() === 'sites') { $projectId = $project->getId(); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php index de6750fe64..c614c80041 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Platform\Scope\HTTP; @@ -235,7 +236,7 @@ class Create extends Action if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds]))); } } } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php index bba062a730..04003812f8 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -114,7 +114,11 @@ class Create extends Action ->setAttribute('personalRefreshToken', $refreshToken) ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([ + 'personalAccessToken' => $installation->getAttribute('personalAccessToken'), + 'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'), + 'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'), + ])); } try { diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php index 38fc611465..a7d16e0a52 100644 --- a/src/Appwrite/Platform/Tasks/Interval.php +++ b/src/Appwrite/Platform/Tasks/Interval.php @@ -159,9 +159,7 @@ class Interval extends Action } foreach ($staleExecutions as $execution) { - $execution->setAttribute('status', 'failed'); - $execution->setAttribute('errors', 'Execution timed out'); - $dbForProject->updateDocument('executions', $execution->getId(), $execution); + $dbForProject->updateDocument('executions', $execution->getId(), new Document(['status' => 'failed', 'errors' => 'Execution timed out'])); } $processed++; diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 7de67a2b66..73509819a9 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -423,7 +423,11 @@ class Certificates extends Action Func $queueForFunctions, Realtime $queueForRealtime ): void { - $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'status' => $rule->getAttribute('status'), + 'certificateId' => $rule->getAttribute('certificateId'), + 'logs' => $rule->getAttribute('logs'), + ])); $projectId = $rule->getAttribute('projectId'); // Skip events for console project (triggered by auto-ssl generation for 1 click setups) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 3fd15478d5..d866cc2bd0 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -365,7 +365,13 @@ class Messaging extends Action $message->setAttribute('deliveredTotal', $deliveredTotal); $message->setAttribute('deliveredAt', DateTime::now()); - $dbForProject->updateDocument('messages', $message->getId(), $message); + $dbForProject->updateDocument('messages', $message->getId(), new Document([ + 'deliveryErrors' => $message->getAttribute('deliveryErrors'), + 'status' => $message->getAttribute('status'), + 'search' => $message->getAttribute('search'), + 'deliveredTotal' => $message->getAttribute('deliveredTotal'), + 'deliveredAt' => $message->getAttribute('deliveredAt'), + ])); // Delete any attachments that were downloaded to local storage if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 7bc6fe8d32..d87edaf788 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -571,11 +571,10 @@ class Migrations extends Action } finally { $message = "Export file size {$sizeMB}MB exceeds your plan limit."; - $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration->setAttribute( - 'errors', - json_encode(['code' => 0, 'message' => $message]), - Document::SET_TYPE_APPEND, - )); + $errors = $migration->getAttribute('errors', []); + $errors[] = json_encode(['code' => 0, 'message' => $message]); + $migration->setAttribute('errors', $errors); + $migration = $this->updateMigrationDocument($migration, $project, $queueForRealtime); $this->sendCSVEmail( success: false, diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index 56839058de..4855a1d4d8 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -168,12 +168,15 @@ class Webhooks extends Action $webhook->setAttribute('logs', $logs); + $updatePayload = ['logs' => $logs]; + if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) { $webhook->setAttribute('enabled', false); + $updatePayload['enabled'] = false; $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan); } - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload)); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $this->errors[] = $logs; @@ -184,8 +187,9 @@ class Webhooks extends Action } else { - $webhook->setAttribute('attempts', 0); // Reset attempts on success - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document([ + 'attempts' => 0, + ])); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $queueForStatsUsage ->addMetric(METRIC_WEBHOOKS_SENT, 1) From f2826189c60387c75f1cac2e3a73b07c9681681b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 6 Mar 2026 18:24:04 +0530 Subject: [PATCH 22/26] fix: remove asserting z does not exist in truncated logs --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 53844fe2c8..48208cc7b1 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2771,7 +2771,6 @@ class FunctionsCustomServerTest extends Scope $this->assertLessThanOrEqual(APP_FUNCTION_LOG_LENGTH_LIMIT, strlen($logs)); $this->assertStringStartsWith('[WARNING] Logs truncated', $logs); - $this->assertStringNotContainsString('z', $logs); $this->assertStringContainsString('a', $logs); // Verify errors are truncated and warning message is present at the beginning @@ -2779,7 +2778,6 @@ class FunctionsCustomServerTest extends Scope $this->assertLessThanOrEqual(APP_FUNCTION_ERROR_LENGTH_LIMIT, strlen($errors)); $this->assertStringStartsWith('[WARNING] Errors truncated', $errors); - $this->assertStringNotContainsString('z', $errors); $this->assertStringContainsString('a', $errors); $this->cleanupFunction($functionId); From 16ad05792d5b99059b8a959c00e1c17f632eecf5 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:58:28 +0000 Subject: [PATCH 23/26] Change blocked user/resource errors from 401 to 403 - Update USER_BLOCKED from 401 to 403 and simplify description - Update GENERAL_RESOURCE_BLOCKED from 401 to 403 Rationale: 403 Forbidden is the correct HTTP status for authorization failures where the user is authenticated but not permitted access. 401 Unauthorized is for authentication failures. Co-Authored-By: Claude Sonnet 4.5 --- app/config/errors.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index bf0f4461f6..e8519fd797 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -27,7 +27,7 @@ return [ Exception::GENERAL_RESOURCE_BLOCKED => [ 'name' => Exception::GENERAL_RESOURCE_BLOCKED, 'description' => 'Access to this resource is blocked.', - 'code' => 401, + 'code' => 403, ], Exception::GENERAL_UNKNOWN_ORIGIN => [ 'name' => Exception::GENERAL_UNKNOWN_ORIGIN, @@ -168,8 +168,8 @@ return [ ], Exception::USER_BLOCKED => [ 'name' => Exception::USER_BLOCKED, - 'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.', - 'code' => 401, + 'description' => 'The current user has been blocked.', + 'code' => 403, ], Exception::USER_INVALID_TOKEN => [ 'name' => Exception::USER_INVALID_TOKEN, From a2ad25a00a27163e815f83d0ca2006729f96fc53 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:03:44 +0000 Subject: [PATCH 24/26] Update test assertions for blocked user status code Change expected status code from 401 to 403 for USER_BLOCKED errors to match the semantic change in error codes. Co-Authored-By: Claude Sonnet 4.5 --- tests/e2e/General/HooksTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/General/HooksTest.php b/tests/e2e/General/HooksTest.php index 1e7d87608f..812fb61496 100644 --- a/tests/e2e/General/HooksTest.php +++ b/tests/e2e/General/HooksTest.php @@ -128,7 +128,7 @@ class HooksTest extends Scope 'cookie' => $cookie, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); /** * Test for api controllers @@ -140,7 +140,7 @@ class HooksTest extends Scope 'cookie' => $cookie, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $this->assertEquals(Exception::USER_BLOCKED, $response['body']['type']); /** From b6793dc0b5dce42ab3e0fe2d718805eb901dd9ec Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:43:36 +0000 Subject: [PATCH 25/26] Fix remaining blocked account test assertions Update testBlockedAccount and testSelfBlockedAccount to expect 403 instead of 401 for blocked user responses. These were missed in the previous test assertion update. Co-Authored-By: Claude Sonnet 4.5 --- tests/e2e/Services/Account/AccountCustomClientTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 2ae0b908d7..ea387cff6c 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2360,7 +2360,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, ])); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', @@ -2371,7 +2371,7 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); } @@ -2440,7 +2440,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, ])); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', @@ -2451,7 +2451,7 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); } public function testCreateJWT(): void From c6d476b9786a3d629c0ab332ea2248e1696e6b16 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 8 Mar 2026 16:52:22 +0200 Subject: [PATCH 26/26] catch error --- src/Appwrite/Platform/Workers/StatsResources.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 5048e6f70d..e464455470 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -202,12 +202,23 @@ class StatsResources extends Action $totalFiles = 0; $totalStorage = 0; $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) { - $files = $dbForProject->count('bucket_' . $bucket->getSequence()); + try { + $files = $dbForProject->count('bucket_' . $bucket->getSequence()); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_bucket_{$bucket->getSequence()}"]); + return; + } $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES); $this->createStatsDocuments($region, $metric, $files); - $storage = $dbForProject->sum('bucket_' . $bucket->getSequence(), 'sizeActual'); + try { + $storage = $dbForProject->sum('bucket_' . $bucket->getSequence(), 'sizeActual'); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "sum_for_bucket_{$bucket->getSequence()}"]); + return; + } + $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_STORAGE); $this->createStatsDocuments($region, $metric, $storage);