From 88107ee7b3e02497f024c427796a346e92b087dd Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 20 Apr 2026 11:57:57 +0530 Subject: [PATCH 01/97] feat: implement custom triggers for VCS builds --- app/config/collections/projects.php | 44 +++++++++++ composer.lock | 15 +++- .../Http/GitHub/Authorize/External/Update.php | 5 +- .../Modules/VCS/Http/GitHub/Deployment.php | 74 +++++++++++++++++++ .../Modules/VCS/Http/GitHub/Events/Create.php | 10 ++- 5 files changed, 140 insertions(+), 8 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 9568c59369..9ac5c562e3 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -841,6 +841,28 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1320,6 +1342,28 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/composer.lock b/composer.lock index d0d69bd0c5..d3d9600e5d 100644 --- a/composer.lock +++ b/composer.lock @@ -8441,9 +8441,18 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/vcs", + "version": "dev-ser-401", + "alias": "3.0.99", + "alias_normalized": "3.0.99.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/vcs": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -8464,5 +8473,5 @@ "platform-dev": { "ext-fileinfo": "*" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } 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 8b320535e9..b3ff4f96fa 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 @@ -130,7 +130,10 @@ class Update extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? ''; - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = array_column($prFiles, 'filename'); + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 6e1db12c28..e5c138f575 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -40,6 +40,7 @@ trait Deployment string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, + array $providerAffectedFiles = [], bool $external, Database $dbForPlatform, Authorization $authorization, @@ -94,6 +95,11 @@ trait Deployment $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resourceInternalId = $resource->getSequence(); + if (!$this->isResourceBuildable($resource, $providerBranch, $providerAffectedFiles, $logBase)) { + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); @@ -560,4 +566,72 @@ trait Deployment { return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); } + + private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool + { + $allowedBranches = $resource->getAttribute('providerBranches', []); + if (!$this->matchesPatterns($providerBranch, $allowedBranches)) { + Span::add("{$logBase}.build.skipped.reason", 'branch'); + return false; + } + + $allowedPaths = $resource->getAttribute('providerPaths', []); + if (!empty($allowedPaths) && !empty($providerAffectedFiles)) { + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($this->matchesPatterns($file, $allowedPaths)) { + $pathMatched = true; + break; + } + } + if (!$pathMatched) { + Span::add("{$logBase}.build.skipped.reason", 'path'); + return false; + } + } + + return true; + } + + private function matchesPatterns(string $subject, array $patterns): bool + { + if (empty($patterns)) { + return true; + } + + $include = array_filter($patterns, fn ($p) => !str_starts_with($p, '!')); + $exclude = array_filter($patterns, fn ($p) => str_starts_with($p, '!')); + + foreach ($include as $pattern) { + if ($this->matchGlob($subject, $pattern)) { + return true; + } + } + + foreach ($exclude as $pattern) { + if ($this->matchGlob($subject, substr($pattern, 1))) { + return false; + } + } + + return empty($include); + } + + private function matchGlob(string $subject, string $pattern): bool + { + $regex = preg_replace_callback( + '/\*\*|\*|\?|[^*?]+/', + static function (array $m): string { + return match ($m[0]) { + '**' => '.*', + '*' => '[^/]*', + '?' => '[^/]', + default => preg_quote($m[0], '/'), + }; + }, + $pattern + ); + + return (bool) preg_match('/^' . $regex . '$/', $subject); + } } 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 e3dbcfa0e9..b04a4e7c5e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -162,9 +162,10 @@ class Create extends Action Query::limit(100), ])); - // Create new deployment only on push (not committed by us) and not when branch is deleted - if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) { - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + // Create new deployment only on push (not committed by us) and not when branch is created or deleted + if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', $providerAffectedFiles, false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } } @@ -216,7 +217,8 @@ class Create extends Action Query::orderDesc('$createdAt') ])); - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } elseif ($action == "closed") { // Allowed external contributions cleanup From 076e64886ee5dcac26661f8ee5317183856307d0 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 13:03:48 +0530 Subject: [PATCH 02/97] validator pattern --- .../Modules/VCS/Http/GitHub/Deployment.php | 60 ++---------- src/Appwrite/Vcs/Validator/BuildTrigger.php | 91 +++++++++++++++++++ tests/unit/Vcs/Validator/BuildTriggerTest.php | 86 ++++++++++++++++++ 3 files changed, 183 insertions(+), 54 deletions(-) create mode 100644 src/Appwrite/Vcs/Validator/BuildTrigger.php create mode 100644 tests/unit/Vcs/Validator/BuildTriggerTest.php diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index e5c138f575..f4c522fb44 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -7,6 +7,7 @@ use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Vcs\Comment; +use Appwrite\Vcs\Validator\BuildTrigger; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Database; @@ -569,22 +570,15 @@ trait Deployment private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool { - $allowedBranches = $resource->getAttribute('providerBranches', []); - if (!$this->matchesPatterns($providerBranch, $allowedBranches)) { + $branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', [])); + if (!$branchTrigger->isValid($providerBranch)) { Span::add("{$logBase}.build.skipped.reason", 'branch'); return false; } - $allowedPaths = $resource->getAttribute('providerPaths', []); - if (!empty($allowedPaths) && !empty($providerAffectedFiles)) { - $pathMatched = false; - foreach ($providerAffectedFiles as $file) { - if ($this->matchesPatterns($file, $allowedPaths)) { - $pathMatched = true; - break; - } - } - if (!$pathMatched) { + $pathTrigger = new BuildTrigger($resource->getAttribute('providerPaths', [])); + foreach ($providerAffectedFiles as $file) { + if (!$pathTrigger->isValid($file)) { Span::add("{$logBase}.build.skipped.reason", 'path'); return false; } @@ -592,46 +586,4 @@ trait Deployment return true; } - - private function matchesPatterns(string $subject, array $patterns): bool - { - if (empty($patterns)) { - return true; - } - - $include = array_filter($patterns, fn ($p) => !str_starts_with($p, '!')); - $exclude = array_filter($patterns, fn ($p) => str_starts_with($p, '!')); - - foreach ($include as $pattern) { - if ($this->matchGlob($subject, $pattern)) { - return true; - } - } - - foreach ($exclude as $pattern) { - if ($this->matchGlob($subject, substr($pattern, 1))) { - return false; - } - } - - return empty($include); - } - - private function matchGlob(string $subject, string $pattern): bool - { - $regex = preg_replace_callback( - '/\*\*|\*|\?|[^*?]+/', - static function (array $m): string { - return match ($m[0]) { - '**' => '.*', - '*' => '[^/]*', - '?' => '[^/]', - default => preg_quote($m[0], '/'), - }; - }, - $pattern - ); - - return (bool) preg_match('/^' . $regex . '$/', $subject); - } } diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php new file mode 100644 index 0000000000..44296ae73a --- /dev/null +++ b/src/Appwrite/Vcs/Validator/BuildTrigger.php @@ -0,0 +1,91 @@ +patterns)) { + return true; + } + + $include = array_filter($this->patterns, fn ($p) => !str_starts_with($p, '!')); + $exclude = array_filter($this->patterns, fn ($p) => str_starts_with($p, '!')); + + foreach ($include as $pattern) { + if ($this->matchGlob($value, $pattern)) { + return true; + } + } + + foreach ($exclude as $pattern) { + if ($this->matchGlob($value, substr($pattern, 1))) { + return false; + } + } + + return empty($include); + } + + public function getDescription(): string + { + return 'Value must match at least one inclusion pattern and must not match any exclusion pattern.'; + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_STRING; + } + + private function matchGlob(string $subject, string $pattern): bool + { + $regex = ''; + $len = strlen($pattern); + $i = 0; + + while ($i < $len) { + $char = $pattern[$i]; + + if ($char === '*' && isset($pattern[$i + 1]) && $pattern[$i + 1] === '*') { + $prevSlash = $i === 0 || $pattern[$i - 1] === '/'; + $nextSlash = isset($pattern[$i + 2]) && $pattern[$i + 2] === '/'; + + if ($prevSlash && $nextSlash) { + // a/**/b → zero or more intermediate dirs (matches a/b, a/x/b, a/x/y/b) + // **/foo → zero or more leading dirs (matches foo, a/foo, a/b/foo) + $regex .= '(?:.+/)?'; + $i += 3; // consume ** and the trailing / + } else { + // foo/** → everything inside (matches foo/a, foo/a/b) + $regex .= '.*'; + $i += 2; + } + } elseif ($char === '*') { + $regex .= '[^/]*'; // anything except a path separator + $i++; + } elseif ($char === '?') { + $regex .= '[^/]'; // any single character except a path separator + $i++; + } else { + $regex .= preg_quote($char, '~'); + $i++; + } + } + + return (bool) preg_match('~^' . $regex . '$~', $subject); + } +} diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php new file mode 100644 index 0000000000..7dbf4e8204 --- /dev/null +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -0,0 +1,86 @@ +assertTrue($validator->isValid('anything')); + $this->assertTrue($validator->isValid('main')); + } + + public function testExactMatch(): void + { + $validator = new BuildTrigger(['main']); + $this->assertTrue($validator->isValid('main')); + $this->assertFalse($validator->isValid('develop')); + } + + public function testSingleWildcard(): void + { + $validator = new BuildTrigger(['feature/*']); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross / + $this->assertFalse($validator->isValid('main')); + } + + public function testDashInPattern(): void + { + $validator = new BuildTrigger(['feature/test-*']); + $this->assertTrue($validator->isValid('feature/test-1')); + $this->assertTrue($validator->isValid('feature/test-abc')); + $this->assertFalse($validator->isValid('feature/other')); + } + + public function testDoubleWildcardEnd(): void + { + $validator = new BuildTrigger(['src/**']); + $this->assertTrue($validator->isValid('src/foo.js')); + $this->assertTrue($validator->isValid('src/a/b/c.js')); + } + + public function testDoubleWildcardMiddle(): void + { + $validator = new BuildTrigger(['a/**/b']); + $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs + $this->assertTrue($validator->isValid('a/x/b')); // one + $this->assertTrue($validator->isValid('a/x/y/b')); // two + $this->assertFalse($validator->isValid('a/b/c')); + } + + public function testDoubleWildcardStart(): void + { + $validator = new BuildTrigger(['**/foo']); + $this->assertTrue($validator->isValid('foo')); + $this->assertTrue($validator->isValid('a/foo')); + $this->assertTrue($validator->isValid('a/b/foo')); + $this->assertFalse($validator->isValid('foobar')); + } + + public function testOrSemantics(): void + { + $validator = new BuildTrigger(['main', 'develop']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); + $this->assertFalse($validator->isValid('feature/x')); + } + + public function testInclusionTakesPrecedenceOverExclusion(): void + { + $validator = new BuildTrigger(['!feature/*', 'feature/abc']); + $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins + $this->assertFalse($validator->isValid('feature/xyz')); // excluded + } + + public function testOnlyExclusions(): void + { + $validator = new BuildTrigger(['!main']); + $this->assertFalse($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); // not excluded, passes by default + } +} From abad809a91580ff72b9ce5d9e9f84a880d32f840 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 13:23:12 +0530 Subject: [PATCH 03/97] get PR files in Appwrite --- .../Platform/Modules/VCS/Http/GitHub/Events/Create.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 b04a4e7c5e..09929cc64c 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -212,12 +212,14 @@ class Create extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; + $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = array_column($prFiles, 'filename'); + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } elseif ($action == "closed") { // Allowed external contributions cleanup From 772358ff5182587a5c59b1620c96ebbdc092bad4 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 13:38:51 +0530 Subject: [PATCH 04/97] more tests --- tests/unit/Vcs/Validator/BuildTriggerTest.php | 186 +++++++++++++++--- 1 file changed, 161 insertions(+), 25 deletions(-) diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php index 7dbf4e8204..61d839cf99 100644 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -7,80 +7,216 @@ use PHPUnit\Framework\TestCase; class BuildTriggerTest extends TestCase { - public function testEmptyPatterns(): void + // ------------------------------------------------------------------------- + // Empty patterns + // ------------------------------------------------------------------------- + + public function testEmptyPatternsAlwaysPass(): void { $validator = new BuildTrigger([]); - $this->assertTrue($validator->isValid('anything')); $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('feature/anything')); + $this->assertTrue($validator->isValid('src/deep/nested/file.php')); } - public function testExactMatch(): void + // ------------------------------------------------------------------------- + // Pure inclusion — OR semantics (any one match is enough) + // ------------------------------------------------------------------------- + + public function testSingleExactInclusion(): void { $validator = new BuildTrigger(['main']); $this->assertTrue($validator->isValid('main')); $this->assertFalse($validator->isValid('develop')); + $this->assertFalse($validator->isValid('main-extra')); } - public function testSingleWildcard(): void + public function testMultipleExactInclusionsOr(): void + { + $validator = new BuildTrigger(['main', 'develop', 'staging']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); + $this->assertTrue($validator->isValid('staging')); + $this->assertFalse($validator->isValid('feature/foo')); + $this->assertFalse($validator->isValid('production')); + } + + public function testSingleWildcardInclusion(): void { $validator = new BuildTrigger(['feature/*']); $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('feature/bar')); $this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross / $this->assertFalse($validator->isValid('main')); } - public function testDashInPattern(): void + public function testWildcardWithDash(): void { $validator = new BuildTrigger(['feature/test-*']); $this->assertTrue($validator->isValid('feature/test-1')); $this->assertTrue($validator->isValid('feature/test-abc')); $this->assertFalse($validator->isValid('feature/other')); + $this->assertFalse($validator->isValid('feature/test')); } - public function testDoubleWildcardEnd(): void + public function testDoubleWildcardAtEnd(): void { $validator = new BuildTrigger(['src/**']); $this->assertTrue($validator->isValid('src/foo.js')); $this->assertTrue($validator->isValid('src/a/b/c.js')); + $this->assertTrue($validator->isValid('src/deep/nested/file.php')); + $this->assertFalse($validator->isValid('lib/foo.js')); } - public function testDoubleWildcardMiddle(): void + public function testDoubleWildcardInMiddle(): void { $validator = new BuildTrigger(['a/**/b']); - $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs - $this->assertTrue($validator->isValid('a/x/b')); // one + $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs + $this->assertTrue($validator->isValid('a/x/b')); // one $this->assertTrue($validator->isValid('a/x/y/b')); // two $this->assertFalse($validator->isValid('a/b/c')); + $this->assertFalse($validator->isValid('x/a/b')); } - public function testDoubleWildcardStart(): void + public function testDoubleWildcardAtStart(): void { $validator = new BuildTrigger(['**/foo']); - $this->assertTrue($validator->isValid('foo')); - $this->assertTrue($validator->isValid('a/foo')); - $this->assertTrue($validator->isValid('a/b/foo')); + $this->assertTrue($validator->isValid('foo')); // zero leading dirs + $this->assertTrue($validator->isValid('a/foo')); // one + $this->assertTrue($validator->isValid('a/b/foo')); // two $this->assertFalse($validator->isValid('foobar')); + $this->assertFalse($validator->isValid('a/foobar')); } - public function testOrSemantics(): void + public function testMixedExactAndWildcardInclusions(): void { - $validator = new BuildTrigger(['main', 'develop']); + $validator = new BuildTrigger(['main', 'feature/*']); $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertFalse($validator->isValid('feature/x')); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertFalse($validator->isValid('develop')); + $this->assertFalse($validator->isValid('feature/foo/bar')); } - public function testInclusionTakesPrecedenceOverExclusion(): void - { - $validator = new BuildTrigger(['!feature/*', 'feature/abc']); - $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins - $this->assertFalse($validator->isValid('feature/xyz')); // excluded - } + // ------------------------------------------------------------------------- + // Pure exclusion — AND semantics (must not match any exclusion) + // ------------------------------------------------------------------------- - public function testOnlyExclusions(): void + public function testSingleExactExclusion(): void { $validator = new BuildTrigger(['!main']); $this->assertFalse($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); // not excluded, passes by default + $this->assertTrue($validator->isValid('develop')); + $this->assertTrue($validator->isValid('feature/foo')); + } + + public function testMultipleExactExclusionsAnd(): void + { + $validator = new BuildTrigger(['!main', '!develop']); + $this->assertFalse($validator->isValid('main')); + $this->assertFalse($validator->isValid('develop')); + $this->assertTrue($validator->isValid('staging')); // neither excluded + $this->assertTrue($validator->isValid('feature/foo')); + } + + public function testWildcardExclusion(): void + { + $validator = new BuildTrigger(['!feature/*']); + $this->assertFalse($validator->isValid('feature/foo')); + $this->assertFalse($validator->isValid('feature/bar')); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('hotfix/urgent')); // not matched by feature/* + } + + public function testDoubleWildcardExclusion(): void + { + $validator = new BuildTrigger(['!src/**']); + $this->assertFalse($validator->isValid('src/foo.js')); + $this->assertFalse($validator->isValid('src/a/b/c.js')); + $this->assertTrue($validator->isValid('lib/foo.js')); + $this->assertTrue($validator->isValid('main')); + } + + // ------------------------------------------------------------------------- + // Mixed inclusion + exclusion + // ------------------------------------------------------------------------- + + public function testInclusionTakesPrecedenceWhenBothMatch(): void + { + // feature/abc matches both '!feature/*' (exclusion) and 'feature/abc' (inclusion) + // Inclusion is checked first, so it wins + $validator = new BuildTrigger(['!feature/*', 'feature/abc']); + $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins + $this->assertFalse($validator->isValid('feature/xyz')); // only exclusion matches + $this->assertFalse($validator->isValid('main')); // no inclusion matches + } + + public function testInclusionWithNoMatchFails(): void + { + // Inclusions exist but none match — exclusion is irrelevant + $validator = new BuildTrigger(['main', '!develop']); + $this->assertTrue($validator->isValid('main')); + $this->assertFalse($validator->isValid('develop')); // excluded even if inclusion didn't match + $this->assertFalse($validator->isValid('staging')); // no inclusion match + } + + public function testExclusionBlocksWhenInclusionDoesNotMatch(): void + { + $validator = new BuildTrigger(['feature/*', '!hotfix/*']); + $this->assertTrue($validator->isValid('feature/foo')); // matches inclusion + $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match, also excluded + $this->assertFalse($validator->isValid('main')); // no inclusion match + } + + public function testMultipleInclusionsWithSingleExclusion(): void + { + // feature/wip matches the inclusion feature/* → true (exclusion !feature/wip is never reached) + $validator = new BuildTrigger(['main', 'develop', 'feature/*', '!feature/wip']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins + $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match + } + + public function testSingleInclusionWithMultipleExclusions(): void + { + // feature/wip and feature/experimental both match the inclusion feature/** → true + $validator = new BuildTrigger(['feature/**', '!feature/wip', '!feature/experimental']); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('feature/a/b')); + $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins + $this->assertTrue($validator->isValid('feature/experimental')); // inclusion wins + $this->assertFalse($validator->isValid('main')); // no inclusion match + } + + public function testMultipleInclusionsWithMultipleExclusions(): void + { + // feature/wip and feature/experimental both match the inclusion feature/** → true + $validator = new BuildTrigger(['main', 'feature/**', '!feature/wip', '!feature/experimental']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('feature/a/b')); + $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins + $this->assertTrue($validator->isValid('feature/experimental')); // inclusion wins + $this->assertFalse($validator->isValid('develop')); // no inclusion match + } + + public function testSpecificInclusionOverridesWildcardExclusion(): void + { + // Narrow allowlist with a carve-out: exclude all of feature/* except feature/hotfix/critical + $validator = new BuildTrigger(['feature/hotfix/critical', '!feature/**']); + $this->assertTrue($validator->isValid('feature/hotfix/critical')); // inclusion wins + $this->assertFalse($validator->isValid('feature/foo')); // excluded + $this->assertFalse($validator->isValid('feature/hotfix/other')); // excluded + $this->assertFalse($validator->isValid('main')); // no inclusion match + } + + public function testOnlyExclusionsDefaultToTrueUnlessExcluded(): void + { + $validator = new BuildTrigger(['!main', '!develop']); + $this->assertFalse($validator->isValid('main')); + $this->assertFalse($validator->isValid('develop')); + $this->assertTrue($validator->isValid('staging')); // passes — no inclusions required + $this->assertTrue($validator->isValid('feature/foo')); } } From 175fcb51a8eb289cb2d5362b6d2474d06a0846a9 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 13:41:52 +0530 Subject: [PATCH 05/97] remove default --- src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index f4c522fb44..3f39ac9018 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -41,7 +41,7 @@ trait Deployment string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, - array $providerAffectedFiles = [], + array $providerAffectedFiles, bool $external, Database $dbForPlatform, Authorization $authorization, From a4f174ea7ed47eff656b953325d69c66c4fb6ecd Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 14:21:32 +0530 Subject: [PATCH 06/97] update VCS --- composer.lock | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index d3d9600e5d..cdbf3c535d 100644 --- a/composer.lock +++ b/composer.lock @@ -8441,18 +8441,9 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [ - { - "package": "utopia-php/vcs", - "version": "dev-ser-401", - "alias": "3.0.99", - "alias_normalized": "3.0.99.0" - } - ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/vcs": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From e17cdc2bd47602f3942ff23e9a5c0e2aaba290fa Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 16:31:42 +0530 Subject: [PATCH 07/97] fix pattern logic --- src/Appwrite/Vcs/Validator/BuildTrigger.php | 32 ++++++++++-- tests/unit/Vcs/Validator/BuildTriggerTest.php | 50 +++++++++++++++---- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php index 44296ae73a..0cad312e7b 100644 --- a/src/Appwrite/Vcs/Validator/BuildTrigger.php +++ b/src/Appwrite/Vcs/Validator/BuildTrigger.php @@ -21,24 +21,48 @@ class BuildTrigger extends Validator $include = array_filter($this->patterns, fn ($p) => !str_starts_with($p, '!')); $exclude = array_filter($this->patterns, fn ($p) => str_starts_with($p, '!')); + if (empty($include)) { + // Only exclusions: pass everything unless excluded. + foreach ($exclude as $pattern) { + if ($this->matchGlob($value, substr($pattern, 1))) { + return false; + } + } + return true; + } + + // A pattern is "specific" when it contains no wildcard characters. + $isSpecific = fn($p) => !str_contains($p, '*') && !str_contains($p, '?'); + + // 1. Specific inclusion always wins — an explicit exact match is never blocked. foreach ($include as $pattern) { - if ($this->matchGlob($value, $pattern)) { + if ($isSpecific($pattern) && $this->matchGlob($value, $pattern)) { return true; } } + // 2. Specific exclusion overrides a wildcard inclusion — refines broad patterns. foreach ($exclude as $pattern) { - if ($this->matchGlob($value, substr($pattern, 1))) { + $raw = substr($pattern, 1); + if ($isSpecific($raw) && $this->matchGlob($value, $raw)) { return false; } } - return empty($include); + // 3. Wildcard inclusion wins over any remaining wildcard exclusion. + foreach ($include as $pattern) { + if (!$isSpecific($pattern) && $this->matchGlob($value, $pattern)) { + return true; + } + } + + // No inclusion matched. + return false; } public function getDescription(): string { - return 'Value must match at least one inclusion pattern and must not match any exclusion pattern.'; + return 'Value must match a specific inclusion, or a wildcard inclusion not overridden by a specific exclusion.'; } public function isArray(): bool diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php index 61d839cf99..a359a570ac 100644 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -59,6 +59,34 @@ class BuildTriggerTest extends TestCase $this->assertFalse($validator->isValid('feature/test')); } + public function testQuestionMarkWildcard(): void + { + $validator = new BuildTrigger(['v?.?']); + $this->assertTrue($validator->isValid('v1.0')); + $this->assertTrue($validator->isValid('v2.5')); + $this->assertFalse($validator->isValid('v10.0')); // ? matches exactly one char, not two + $this->assertFalse($validator->isValid('v1/0')); // ? does not cross / + } + + public function testQuestionMarkDoesNotCrossSlash(): void + { + $validator = new BuildTrigger(['feature/?']); + $this->assertTrue($validator->isValid('feature/a')); + $this->assertTrue($validator->isValid('feature/z')); + $this->assertFalse($validator->isValid('feature/ab')); // ? matches only one char + $this->assertFalse($validator->isValid('feature/a/b')); // ? does not cross / + $this->assertFalse($validator->isValid('feature/')); + } + + public function testQuestionMarkMixedWithStar(): void + { + $validator = new BuildTrigger(['fix-?.*']); + $this->assertTrue($validator->isValid('fix-1.php')); + $this->assertTrue($validator->isValid('fix-a.js')); + $this->assertFalse($validator->isValid('fix-12.php')); // ? matches only one char + $this->assertFalse($validator->isValid('fix-.php')); // ? requires exactly one char + } + public function testDoubleWildcardAtEnd(): void { $validator = new BuildTrigger(['src/**']); @@ -169,36 +197,36 @@ class BuildTriggerTest extends TestCase public function testMultipleInclusionsWithSingleExclusion(): void { - // feature/wip matches the inclusion feature/* → true (exclusion !feature/wip is never reached) + // feature/wip matches wildcard inclusion feature/* but specific exclusion !feature/wip overrides it $validator = new BuildTrigger(['main', 'develop', 'feature/*', '!feature/wip']); $this->assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('develop')); $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins + $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion overrides wildcard inclusion $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match } public function testSingleInclusionWithMultipleExclusions(): void { - // feature/wip and feature/experimental both match the inclusion feature/** → true + // specific exclusions !feature/wip and !feature/experimental override wildcard inclusion feature/** $validator = new BuildTrigger(['feature/**', '!feature/wip', '!feature/experimental']); $this->assertTrue($validator->isValid('feature/foo')); $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins - $this->assertTrue($validator->isValid('feature/experimental')); // inclusion wins - $this->assertFalse($validator->isValid('main')); // no inclusion match + $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins + $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins + $this->assertFalse($validator->isValid('main')); // no inclusion match } public function testMultipleInclusionsWithMultipleExclusions(): void { - // feature/wip and feature/experimental both match the inclusion feature/** → true + // specific exclusions override the wildcard inclusion; specific inclusion 'main' is unaffected $validator = new BuildTrigger(['main', 'feature/**', '!feature/wip', '!feature/experimental']); - $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('main')); // specific inclusion wins regardless $this->assertTrue($validator->isValid('feature/foo')); $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertTrue($validator->isValid('feature/wip')); // inclusion wins - $this->assertTrue($validator->isValid('feature/experimental')); // inclusion wins - $this->assertFalse($validator->isValid('develop')); // no inclusion match + $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins + $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins + $this->assertFalse($validator->isValid('develop')); // no inclusion match } public function testSpecificInclusionOverridesWildcardExclusion(): void From a70d241da049abead910561f0b9bff06fca68e65 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 16:37:31 +0530 Subject: [PATCH 08/97] fix path trigger --- .../Modules/VCS/Http/GitHub/Deployment.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 3f39ac9018..c30f960262 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -576,9 +576,17 @@ trait Deployment return false; } - $pathTrigger = new BuildTrigger($resource->getAttribute('providerPaths', [])); - foreach ($providerAffectedFiles as $file) { - if (!$pathTrigger->isValid($file)) { + $providerPaths = $resource->getAttribute('providerPaths', []); + if (!empty($providerPaths) && !empty($providerAffectedFiles)) { + $pathTrigger = new BuildTrigger($providerPaths); + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($pathTrigger->isValid($file)) { + $pathMatched = true; + break; + } + } + if (!$pathMatched) { Span::add("{$logBase}.build.skipped.reason", 'path'); return false; } From 87f29c688932426a587520d1104199a7dccaee21 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 16:48:37 +0530 Subject: [PATCH 09/97] include previous_filename --- .../Modules/VCS/Http/GitHub/Authorize/External/Update.php | 5 ++++- .../Platform/Modules/VCS/Http/GitHub/Events/Create.php | 5 ++++- src/Appwrite/Vcs/Validator/BuildTrigger.php | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) 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 b3ff4f96fa..56230b7de3 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 @@ -131,7 +131,10 @@ class Update extends Action $providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? ''; $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); - $providerAffectedFiles = array_column($prFiles, 'filename'); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + ...array_column($prFiles, 'previous_filename') + ]; $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); 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 09929cc64c..28aeb8c140 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -213,7 +213,10 @@ class Create extends Action $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); - $providerAffectedFiles = array_column($prFiles, 'filename'); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + ...array_column($prFiles, 'previous_filename') + ]; $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php index 0cad312e7b..842d81a455 100644 --- a/src/Appwrite/Vcs/Validator/BuildTrigger.php +++ b/src/Appwrite/Vcs/Validator/BuildTrigger.php @@ -6,7 +6,9 @@ use Utopia\Validator; class BuildTrigger extends Validator { - public function __construct(private readonly array $patterns) {} + public function __construct(private readonly array $patterns) + { + } public function isValid($value): bool { @@ -32,7 +34,7 @@ class BuildTrigger extends Validator } // A pattern is "specific" when it contains no wildcard characters. - $isSpecific = fn($p) => !str_contains($p, '*') && !str_contains($p, '?'); + $isSpecific = fn ($p) => !str_contains($p, '*') && !str_contains($p, '?'); // 1. Specific inclusion always wins — an explicit exact match is never blocked. foreach ($include as $pattern) { From e802102c8f08419720c1ee0599367c22a2723ba0 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 17:09:38 +0530 Subject: [PATCH 10/97] fix exclusion sub-directory --- src/Appwrite/Vcs/Validator/BuildTrigger.php | 11 +++++------ tests/unit/Vcs/Validator/BuildTriggerTest.php | 11 +++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php index 842d81a455..22037c8907 100644 --- a/src/Appwrite/Vcs/Validator/BuildTrigger.php +++ b/src/Appwrite/Vcs/Validator/BuildTrigger.php @@ -34,7 +34,7 @@ class BuildTrigger extends Validator } // A pattern is "specific" when it contains no wildcard characters. - $isSpecific = fn ($p) => !str_contains($p, '*') && !str_contains($p, '?'); + $isSpecific = fn ($pattern) => !str_contains($pattern, '*') && !str_contains($pattern, '?'); // 1. Specific inclusion always wins — an explicit exact match is never blocked. foreach ($include as $pattern) { @@ -43,15 +43,14 @@ class BuildTrigger extends Validator } } - // 2. Specific exclusion overrides a wildcard inclusion — refines broad patterns. + // 2. Any exclusion (specific or wildcard) overrides a wildcard inclusion — refines broad patterns. foreach ($exclude as $pattern) { - $raw = substr($pattern, 1); - if ($isSpecific($raw) && $this->matchGlob($value, $raw)) { + if ($this->matchGlob($value, substr($pattern, 1))) { return false; } } - // 3. Wildcard inclusion wins over any remaining wildcard exclusion. + // 3. Wildcard inclusion — no exclusion blocked it. foreach ($include as $pattern) { if (!$isSpecific($pattern) && $this->matchGlob($value, $pattern)) { return true; @@ -64,7 +63,7 @@ class BuildTrigger extends Validator public function getDescription(): string { - return 'Value must match a specific inclusion, or a wildcard inclusion not overridden by a specific exclusion.'; + return 'Value must match a specific inclusion, or a wildcard inclusion not overridden by any exclusion.'; } public function isArray(): bool diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php index a359a570ac..b26033b20b 100644 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -229,6 +229,17 @@ class BuildTriggerTest extends TestCase $this->assertFalse($validator->isValid('develop')); // no inclusion match } + public function testWildcardExclusionOverridesWildcardInclusion(): void + { + // src/** is a broad inclusion; !src/generated/** carves out the generated subtree + $validator = new BuildTrigger(['src/**', '!src/generated/**']); + $this->assertTrue($validator->isValid('src/components/Button.php')); + $this->assertTrue($validator->isValid('src/utils/helper.js')); + $this->assertFalse($validator->isValid('src/generated/Foo.php')); // wildcard exclusion wins + $this->assertFalse($validator->isValid('src/generated/bar/Baz.php')); // wildcard exclusion wins + $this->assertFalse($validator->isValid('lib/other.php')); // no inclusion match + } + public function testSpecificInclusionOverridesWildcardExclusion(): void { // Narrow allowlist with a carve-out: exclude all of feature/* except feature/hotfix/critical From 60e51548e8983f4e719ba0e2261df8c3adfbff5b Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 17:09:45 +0530 Subject: [PATCH 11/97] more tests --- tests/unit/Vcs/Validator/BuildTriggerTest.php | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php index b26033b20b..66aa94abed 100644 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -258,4 +258,209 @@ class BuildTriggerTest extends TestCase $this->assertTrue($validator->isValid('staging')); // passes — no inclusions required $this->assertTrue($validator->isValid('feature/foo')); } + + // ------------------------------------------------------------------------- + // Standalone wildcards + // ------------------------------------------------------------------------- + + public function testStarAloneMatchesSingleSegmentOnly(): void + { + // * → ^[^/]*$ — matches any single segment; the [^/]* guard prevents crossing / + $validator = new BuildTrigger(['*']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); + $this->assertFalse($validator->isValid('feature/foo')); // * cannot cross / + $this->assertFalse($validator->isValid('a/b/c')); + } + + public function testDoubleStarAloneMatchesEverything(): void + { + // ** alone → ^.*$ — matches any string including paths with separators + $validator = new BuildTrigger(['**']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('src/a/b/c/d/file.php')); + } + + // ------------------------------------------------------------------------- + // Extension patterns — * scope vs. ** scope + // ------------------------------------------------------------------------- + + public function testStarDotExtMatchesRootLevelOnly(): void + { + // *.php → ^[^/]*\.php$ — root-level only; * cannot cross / + $validator = new BuildTrigger(['*.php']); + $this->assertTrue($validator->isValid('Foo.php')); + $this->assertTrue($validator->isValid('index.php')); + $this->assertFalse($validator->isValid('src/Foo.php')); // * does not cross / + $this->assertFalse($validator->isValid('a/b/Foo.php')); + $this->assertFalse($validator->isValid('Foo.js')); + } + + public function testDoubleStarSlashExtMatchesAnyDepth(): void + { + // **/*.php → ^(?:.+/)?[^/]*\.php$ — (?:.+/)? is optional so root also matches + $validator = new BuildTrigger(['**/*.php']); + $this->assertTrue($validator->isValid('Foo.php')); // root — zero leading dirs + $this->assertTrue($validator->isValid('src/Foo.php')); + $this->assertTrue($validator->isValid('src/components/Foo.php')); + $this->assertTrue($validator->isValid('a/b/c/d/Foo.php')); // four levels deep + $this->assertFalse($validator->isValid('Foo.js')); + $this->assertFalse($validator->isValid('src/Foo.js')); + } + + public function testDirPrefixDoubleStarExtPattern(): void + { + // src/**/*.php → ^src/(?:.+/)?[^/]*\.php$ — scoped to src/ at any depth + $validator = new BuildTrigger(['src/**/*.php']); + $this->assertTrue($validator->isValid('src/Foo.php')); + $this->assertTrue($validator->isValid('src/components/Foo.php')); + $this->assertTrue($validator->isValid('src/a/b/c/Foo.php')); + $this->assertFalse($validator->isValid('Foo.php')); // outside src/ + $this->assertFalse($validator->isValid('lib/Foo.php')); + $this->assertFalse($validator->isValid('src/Foo.js')); + } + + // ------------------------------------------------------------------------- + // Dots as literal characters + // ------------------------------------------------------------------------- + + public function testDotsInPatternAreLiteral(): void + { + // preg_quote escapes . to \. — dots are never regex wildcards + $validator = new BuildTrigger(['release-1.0.0']); + $this->assertTrue($validator->isValid('release-1.0.0')); + $this->assertFalse($validator->isValid('release-1X0Y0')); // X/Y must not satisfy a literal dot + $this->assertFalse($validator->isValid('release-1.0.0-hotfix')); // extra suffix rejected by $ anchor + } + + public function testVersionWildcardBranchPattern(): void + { + // v*.*.* → ^v[^/]*\.[^/]*\.[^/]*$ — dots literal; [^/]* is greedy and can consume dots, + // so v1.2.3.4 also matches (first [^/]* eats "1.2") — documents current behavior + $validator = new BuildTrigger(['v*.*.*']); + $this->assertTrue($validator->isValid('v1.2.3')); + $this->assertTrue($validator->isValid('v10.20.30')); + $this->assertTrue($validator->isValid('v1.2.3.4')); // greedy match — first [^/]* eats "1.2" + $this->assertFalse($validator->isValid('v1.2')); // only two dot-segments; third \.[^/]* fails + $this->assertFalse($validator->isValid('1.2.3')); // missing leading v + $this->assertFalse($validator->isValid('v1/2/3')); // [^/]* cannot cross / + } + + public function testDottedFilenamePattern(): void + { + // *.test.js → ^[^/]*\.test\.js$ — both dots literal; * stays in root segment + $validator = new BuildTrigger(['*.test.js']); + $this->assertTrue($validator->isValid('Button.test.js')); + $this->assertTrue($validator->isValid('App.test.js')); + $this->assertFalse($validator->isValid('ButtonXtestYjs')); // dots must be literal + $this->assertFalse($validator->isValid('src/Button.test.js')); // * does not cross / + $this->assertFalse($validator->isValid('Button.test.ts')); + } + + // ------------------------------------------------------------------------- + // Prefix wildcard + // ------------------------------------------------------------------------- + + public function testPrefixWildcardBranchPattern(): void + { + // main* → ^main[^/]*$ — suffix wildcard; [^/]* cannot cross / + $validator = new BuildTrigger(['main*']); + $this->assertTrue($validator->isValid('main')); // zero trailing chars + $this->assertTrue($validator->isValid('main-extra')); + $this->assertTrue($validator->isValid('mainline')); + $this->assertFalse($validator->isValid('main/branch')); // [^/]* cannot cross / + $this->assertFalse($validator->isValid('develop')); + } + + // ------------------------------------------------------------------------- + // Deep nesting + // ------------------------------------------------------------------------- + + public function testDoubleWildcardInMiddleDeepNesting(): void + { + // a/**/b → ^a/(?:.+/)?b$ — .+ inside (?:.+/)? matches any chars including /, + // so it handles arbitrarily many intermediate directories + $validator = new BuildTrigger(['a/**/b']); + $this->assertTrue($validator->isValid('a/x/y/z/b')); // three intermediate dirs + $this->assertTrue($validator->isValid('a/p/q/r/s/b')); // four intermediate dirs + $this->assertTrue($validator->isValid('a/1/2/3/4/5/b')); // five intermediate dirs + $this->assertFalse($validator->isValid('a/x/y/z/b/extra')); // trailing segment rejected + $this->assertFalse($validator->isValid('prefix/a/x/b')); // leading segment rejected + } + + public function testDoubleWildcardAtStartDeepNesting(): void + { + // **/README.md → ^(?:.+/)?README\.md$ — matches at any depth; $ anchor prevents suffixes + $validator = new BuildTrigger(['**/README.md']); + $this->assertTrue($validator->isValid('README.md')); // zero leading dirs + $this->assertTrue($validator->isValid('docs/README.md')); + $this->assertTrue($validator->isValid('a/b/c/d/README.md')); // four levels deep + $this->assertTrue($validator->isValid('x/y/z/w/v/README.md')); // five levels deep + $this->assertFalse($validator->isValid('a/b/c/README.md.bak')); // $ anchor — no suffix + $this->assertFalse($validator->isValid('a/b/c/README.md/extra')); // trailing segment rejected + } + + // ------------------------------------------------------------------------- + // Real-world path patterns + // ------------------------------------------------------------------------- + + public function testGeneratedFilesAnywhereExclusion(): void + { + // !**/generated/** → ^(?:.+/)?generated/.*$ — excludes generated/ at any depth + $validator = new BuildTrigger(['!**/generated/**']); + $this->assertFalse($validator->isValid('generated/Foo.php')); // root generated/ + $this->assertFalse($validator->isValid('src/generated/Foo.php')); + $this->assertFalse($validator->isValid('src/api/generated/Bar.php')); // deep generated/ + $this->assertFalse($validator->isValid('generated/sub/deep/File.php')); // deep inside root generated/ + $this->assertTrue($validator->isValid('src/components/Button.php')); // not under generated/ + $this->assertTrue($validator->isValid('main')); + } + + public function testMultipleExtensionInclusions(): void + { + // OR semantics: any PHP or JS file triggers; other extensions do not + $validator = new BuildTrigger(['**/*.php', '**/*.js']); + $this->assertTrue($validator->isValid('index.php')); + $this->assertTrue($validator->isValid('src/App.php')); + $this->assertTrue($validator->isValid('index.js')); + $this->assertTrue($validator->isValid('src/components/App.js')); + $this->assertFalse($validator->isValid('styles.css')); + $this->assertFalse($validator->isValid('src/styles.css')); + $this->assertFalse($validator->isValid('README.md')); + } + + // ------------------------------------------------------------------------- + // Named-prefix single-level branch + // ------------------------------------------------------------------------- + + public function testReleaseBranchPattern(): void + { + // release/* → ^release/[^/]*$ — one level only; * stops at the next / + $validator = new BuildTrigger(['release/*']); + $this->assertTrue($validator->isValid('release/1.0')); + $this->assertTrue($validator->isValid('release/hotfix')); + $this->assertTrue($validator->isValid('release/2024-01-15')); + $this->assertFalse($validator->isValid('release/1.0/patch')); // * stops at / + $this->assertFalse($validator->isValid('release')); // missing the /* segment + $this->assertFalse($validator->isValid('main')); + } + + // ------------------------------------------------------------------------- + // Case sensitivity + // ------------------------------------------------------------------------- + + public function testPatternMatchingIsCaseSensitive(): void + { + // preg_match is used without the i flag — matching is always case-sensitive + $branchValidator = new BuildTrigger(['main']); + $this->assertTrue($branchValidator->isValid('main')); + $this->assertFalse($branchValidator->isValid('Main')); + $this->assertFalse($branchValidator->isValid('MAIN')); + + $wildcardValidator = new BuildTrigger(['feature/*']); + $this->assertTrue($wildcardValidator->isValid('feature/foo')); + $this->assertFalse($wildcardValidator->isValid('Feature/foo')); + $this->assertFalse($wildcardValidator->isValid('FEATURE/foo')); + } } From aa5543ac0afd259848fa1ded7345a884dd0dd338 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 17:31:48 +0530 Subject: [PATCH 12/97] filter out null values --- .../Modules/VCS/Http/GitHub/Authorize/External/Update.php | 2 +- src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 56230b7de3..99f0e0cadd 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 @@ -133,7 +133,7 @@ class Update extends Action $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), - ...array_column($prFiles, 'previous_filename') + ...array_filter(array_column($prFiles, 'previous_filename')) // Filter out null values ]; $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); 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 28aeb8c140..4c75515aaa 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -215,7 +215,7 @@ class Create extends Action $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), - ...array_column($prFiles, 'previous_filename') + ...array_filter(array_column($prFiles, 'previous_filename')) // Filter out null values ]; $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ From 63f04f893ddfff7c672495ccc5e3ff6131caa2c2 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 20 Apr 2026 12:18:26 +0530 Subject: [PATCH 13/97] fix composer --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index cdbf3c535d..d0d69bd0c5 100644 --- a/composer.lock +++ b/composer.lock @@ -8464,5 +8464,5 @@ "platform-dev": { "ext-fileinfo": "*" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 2d3c1086ae0e15ac60b38a0af6ae9a8d13265926 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 13:50:40 +0530 Subject: [PATCH 14/97] add endpoints --- .../Platform/Modules/Functions/Http/Functions/Create.php | 6 ++++++ .../Platform/Modules/Functions/Http/Functions/Update.php | 6 ++++++ src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php | 7 +++++++ src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php | 7 +++++++ 4 files changed, 26 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 7b294f3f90..f25c48937b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -93,6 +93,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -145,6 +147,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, string $templateRepository, @@ -246,6 +250,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 7d6572d336..5c5d312ae8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -87,6 +87,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -131,6 +133,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -274,6 +278,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$functionId, $name, $runtime]), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index d01d0d8ca7..9f3d46ffd0 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -19,6 +19,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; @@ -78,6 +79,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -118,6 +121,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -173,6 +178,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'buildRuntime' => $buildRuntime, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 3c0d090b7b..32443d2250 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -22,6 +22,7 @@ use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; @@ -81,6 +82,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -125,6 +128,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -269,6 +274,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$siteId, $name, $framework]), From e7405b439362f2471e20916c5dbbe9ecd9b4dc57 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 13:01:00 +0530 Subject: [PATCH 15/97] addressed comments --- .../Modules/Functions/Http/Functions/Update.php | 12 ++++++------ .../Platform/Modules/Sites/Http/Sites/Update.php | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 5c5d312ae8..b165026688 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -87,8 +87,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) - ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) - ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -133,8 +133,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, - array $providerBranches, - array $providerPaths, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -278,8 +278,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, - 'providerBranches' => $providerBranches, - 'providerPaths' => $providerPaths, + 'providerBranches' => $providerBranches ?? $function->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $function->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$functionId, $name, $runtime]), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 32443d2250..fd079fcda8 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -24,6 +24,7 @@ use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; +use Utopia\Validator\Nullable; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -82,8 +83,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) - ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) - ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -128,8 +129,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, - array $providerBranches, - array $providerPaths, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -274,8 +275,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, - 'providerBranches' => $providerBranches, - 'providerPaths' => $providerPaths, + 'providerBranches' => $providerBranches ?? $site->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $site->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$siteId, $name, $framework]), From 40c2e1a1faff5468983b30d7ffafb1d84bd1efac Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 4 May 2026 13:09:38 +0530 Subject: [PATCH 16/97] feat: skip deployment on commit message pattern match Add providerCommitSkipPatterns array field to functions and sites. Any commit message containing one of the patterns (case-insensitive substring) skips the VCS-triggered deployment. Co-Authored-By: Claude Sonnet 4.6 --- app/config/collections/projects.php | 22 +++ .../Modules/VCS/Http/GitHub/Deployment.php | 11 +- .../Vcs/Validator/CommitSkipPatterns.php | 50 +++++++ .../Vcs/Validator/CommitSkipPatternsTest.php | 141 ++++++++++++++++++ 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/Appwrite/Vcs/Validator/CommitSkipPatterns.php create mode 100644 tests/unit/Vcs/Validator/CommitSkipPatternsTest.php diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 9ac5c562e3..72daf91688 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -863,6 +863,17 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('providerCommitSkipPatterns'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1364,6 +1375,17 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('providerCommitSkipPatterns'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 6c59d3c80a..d2b2511a43 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -8,6 +8,7 @@ use Appwrite\Extend\Exception; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Vcs\Comment; use Appwrite\Vcs\Validator\BuildTrigger; +use Appwrite\Vcs\Validator\CommitSkipPatterns; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Database; @@ -96,7 +97,7 @@ trait Deployment $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resourceInternalId = $resource->getSequence(); - if (!$this->isResourceBuildable($resource, $providerBranch, $providerAffectedFiles, $logBase)) { + if (!$this->isResourceBuildable($resource, $providerBranch, $providerAffectedFiles, $logBase, $providerCommitMessage)) { Span::add("{$logBase}.build.skipped", 'true'); continue; } @@ -568,7 +569,7 @@ trait Deployment return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); } - private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool + private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase, string $providerCommitMessage = ''): bool { $branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', [])); if (!$branchTrigger->isValid($providerBranch)) { @@ -592,6 +593,12 @@ trait Deployment } } + $commitSkip = new CommitSkipPatterns($resource->getAttribute('providerCommitSkipPatterns', [])); + if (!$commitSkip->isValid($providerCommitMessage)) { + Span::add("{$logBase}.build.skipped.reason", 'commitMessage'); + return false; + } + return true; } } diff --git a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php new file mode 100644 index 0000000000..76c04e5417 --- /dev/null +++ b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php @@ -0,0 +1,50 @@ +patterns as $pattern) { + if (!is_string($pattern) || $pattern === '') { + continue; + } + if (stripos($value, $pattern) !== false) { + return false; + } + } + + return true; + } + + public function getDescription(): string + { + return 'Commit message must not contain any of the configured skip patterns.'; + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php new file mode 100644 index 0000000000..3725c643bc --- /dev/null +++ b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php @@ -0,0 +1,141 @@ +assertTrue($validator->isValid('fix: update readme')); + $this->assertTrue($validator->isValid('[skip deploy] docs only')); + $this->assertTrue($validator->isValid('')); + } + + // ------------------------------------------------------------------------- + // Single pattern — exact substring + // ------------------------------------------------------------------------- + + public function testSinglePatternMatchSkips(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]']); + $this->assertFalse($validator->isValid('[skip deploy] docs only')); + $this->assertFalse($validator->isValid('chore: update deps [skip deploy]')); + $this->assertFalse($validator->isValid('prefix [skip deploy] suffix')); + } + + public function testSinglePatternNoMatchProceeds(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]']); + $this->assertTrue($validator->isValid('fix: real bug fix')); + $this->assertTrue($validator->isValid('feat: add new feature')); + $this->assertTrue($validator->isValid('skip deploy without brackets')); + } + + // ------------------------------------------------------------------------- + // Case insensitivity + // ------------------------------------------------------------------------- + + public function testCaseInsensitiveMatch(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]']); + $this->assertFalse($validator->isValid('[SKIP DEPLOY] uppercase')); + $this->assertFalse($validator->isValid('[Skip Deploy] mixed case')); + $this->assertFalse($validator->isValid('[skip DEPLOY] partial upper')); + } + + public function testPatternItselfCaseInsensitive(): void + { + $validator = new CommitSkipPatterns(['[SKIP DEPLOY]']); + $this->assertFalse($validator->isValid('[skip deploy] lowercase message')); + $this->assertFalse($validator->isValid('[Skip Deploy] mixed message')); + } + + // ------------------------------------------------------------------------- + // Array of patterns — any match skips (OR semantics) + // ------------------------------------------------------------------------- + + public function testMultiplePatternsFirstMatches(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); + $this->assertFalse($validator->isValid('[skip deploy] docs only')); + } + + public function testMultiplePatternsSecondMatches(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); + $this->assertFalse($validator->isValid('chore: update readme [skip ci]')); + } + + public function testMultiplePatternsThirdMatches(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); + $this->assertFalse($validator->isValid('[no deploy] just docs')); + } + + public function testMultiplePatternsNoneMatchProceeds(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); + $this->assertTrue($validator->isValid('feat: completely new feature')); + $this->assertTrue($validator->isValid('fix: important bug fix')); + } + + // ------------------------------------------------------------------------- + // Common real-world skip conventions + // ------------------------------------------------------------------------- + + public function testCommonSkipCiPattern(): void + { + $validator = new CommitSkipPatterns(['[skip ci]']); + $this->assertFalse($validator->isValid('[skip ci] update changelog')); + $this->assertFalse($validator->isValid('[SKIP CI]')); + $this->assertTrue($validator->isValid('feat: something real')); + } + + public function testNoDeployPattern(): void + { + $validator = new CommitSkipPatterns(['[no deploy]']); + $this->assertFalse($validator->isValid('[no deploy] tweak docs')); + $this->assertTrue($validator->isValid('deploy this please')); + } + + // ------------------------------------------------------------------------- + // Edge cases + // ------------------------------------------------------------------------- + + public function testEmptyCommitMessageNeverSkipsWithPatterns(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]']); + $this->assertTrue($validator->isValid('')); + } + + public function testBlankPatternsInArrayAreIgnored(): void + { + $validator = new CommitSkipPatterns(['', ' ', '[skip deploy]']); + // empty/whitespace-only patterns must not cause a false positive on empty messages + $this->assertTrue($validator->isValid('normal commit message')); + // but the real pattern still works + $this->assertFalse($validator->isValid('[skip deploy] docs')); + } + + public function testPatternAsSubstringOfLongerWord(): void + { + // "skip" is a substring of "skippy" — should NOT accidentally skip + $validator = new CommitSkipPatterns(['[skip deploy]']); + $this->assertTrue($validator->isValid('skippy the kangaroo')); + } + + public function testMultilineCommitMessage(): void + { + $validator = new CommitSkipPatterns(['[skip deploy]']); + $msg = "feat: add new stuff\n\nMore detail here.\n\n[skip deploy]"; + $this->assertFalse($validator->isValid($msg)); + } +} From 61cee5892afdb89e8a629b365991f0b09255da57 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 5 May 2026 18:23:14 +0530 Subject: [PATCH 17/97] fix: tighten commit skip directive matching --- .../Vcs/Validator/CommitSkipPatterns.php | 74 +++++++++++++++++-- .../Vcs/Validator/CommitSkipPatternsTest.php | 23 ++++-- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php index 76c04e5417..7243808bf8 100644 --- a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php +++ b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php @@ -12,7 +12,7 @@ class CommitSkipPatterns extends Validator /** * Returns false (skip deployment) when the commit message contains any of the - * configured patterns (case-insensitive substring match). + * configured skip directives. * Returns true (proceed) when no patterns are configured or none match. */ public function isValid($value): bool @@ -21,11 +21,13 @@ class CommitSkipPatterns extends Validator return false; } - foreach ($this->patterns as $pattern) { - if (!is_string($pattern) || $pattern === '') { - continue; - } - if (stripos($value, $pattern) !== false) { + $patterns = $this->normalizePatterns($this->patterns); + if (empty($patterns)) { + return true; + } + + foreach ($this->extractDirectives($value) as $directive) { + if (isset($patterns[$directive])) { return false; } } @@ -47,4 +49,64 @@ class CommitSkipPatterns extends Validator { return self::TYPE_STRING; } + + /** + * @param array $patterns + * @return array + */ + private function normalizePatterns(array $patterns): array + { + $normalized = []; + + foreach ($patterns as $pattern) { + if (!\is_string($pattern)) { + continue; + } + + $pattern = $this->normalizeDirective($pattern); + if ($pattern === '') { + continue; + } + + $normalized[$pattern] = true; + } + + return $normalized; + } + + /** + * @return array + */ + private function extractDirectives(string $message): array + { + $directives = []; + + if (\preg_match_all('/\[[^\]\r\n]+\]/u', $message, $matches) > 0) { + foreach ($matches[0] as $match) { + $directives[] = $this->normalizeDirective($match); + } + } + + foreach (\preg_split("/\r\n|\n|\r/", $message) ?: [] as $line) { + $line = \trim($line); + if ($line === '' || !\str_contains($line, ':')) { + continue; + } + + $directives[] = $this->normalizeDirective($line); + } + + return \array_values(\array_filter(\array_unique($directives))); + } + + private function normalizeDirective(string $value): string + { + $value = \trim($value); + if ($value === '') { + return ''; + } + + $value = (string) \preg_replace('/\s+/u', ' ', $value); + return \mb_strtolower($value); + } } diff --git a/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php index 3725c643bc..efc88bbf6a 100644 --- a/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php +++ b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php @@ -20,7 +20,7 @@ class CommitSkipPatternsTest extends TestCase } // ------------------------------------------------------------------------- - // Single pattern — exact substring + // Single pattern — directive match // ------------------------------------------------------------------------- public function testSinglePatternMatchSkips(): void @@ -37,6 +37,7 @@ class CommitSkipPatternsTest extends TestCase $this->assertTrue($validator->isValid('fix: real bug fix')); $this->assertTrue($validator->isValid('feat: add new feature')); $this->assertTrue($validator->isValid('skip deploy without brackets')); + $this->assertTrue($validator->isValid('prefix[skip deploy]suffix')); } // ------------------------------------------------------------------------- @@ -119,17 +120,15 @@ class CommitSkipPatternsTest extends TestCase public function testBlankPatternsInArrayAreIgnored(): void { $validator = new CommitSkipPatterns(['', ' ', '[skip deploy]']); - // empty/whitespace-only patterns must not cause a false positive on empty messages $this->assertTrue($validator->isValid('normal commit message')); - // but the real pattern still works $this->assertFalse($validator->isValid('[skip deploy] docs')); } - public function testPatternAsSubstringOfLongerWord(): void + public function testPatternMustBeStandaloneDirective(): void { - // "skip" is a substring of "skippy" — should NOT accidentally skip $validator = new CommitSkipPatterns(['[skip deploy]']); $this->assertTrue($validator->isValid('skippy the kangaroo')); + $this->assertTrue($validator->isValid('prefix[skip deploy]suffix')); } public function testMultilineCommitMessage(): void @@ -138,4 +137,18 @@ class CommitSkipPatternsTest extends TestCase $msg = "feat: add new stuff\n\nMore detail here.\n\n[skip deploy]"; $this->assertFalse($validator->isValid($msg)); } + + public function testWhitespaceInsideDirectiveIsNormalized(): void + { + $validator = new CommitSkipPatterns([' [skip deploy] ']); + $this->assertFalse($validator->isValid('[skip deploy] docs only')); + $this->assertFalse($validator->isValid('[SKIP DEPLOY] docs only')); + } + + public function testTrailerDirectiveCanSkip(): void + { + $validator = new CommitSkipPatterns(['skip-checks: true']); + $msg = "feat: add new stuff\n\nMore detail here.\n\nskip-checks:true"; + $this->assertFalse($validator->isValid($msg)); + } } From 90f5f9b30be6f1263d9675c4ca80006361c2bd39 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 7 May 2026 18:25:39 +0530 Subject: [PATCH 18/97] refactor: standalone regex matching for commit skip patterns Replace directive-extraction approach with word-boundary regex matching so plain-word patterns like "skip appwrite" and "appwrite skip" work alongside bracket directives. Use \s+ between word tokens (required space) and \s* only after ":" tokens (git trailer flexibility). Add tests for "skip appwrite" and "appwrite skip" with case insensitivity. Co-Authored-By: Claude Sonnet 4.6 --- .../Vcs/Validator/CommitSkipPatterns.php | 107 +++++++----------- .../Vcs/Validator/CommitSkipPatternsTest.php | 61 ++++++++++ 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php index 7243808bf8..9dbe45ba83 100644 --- a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php +++ b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php @@ -12,8 +12,16 @@ class CommitSkipPatterns extends Validator /** * Returns false (skip deployment) when the commit message contains any of the - * configured skip directives. + * configured patterns as a standalone directive (case-insensitive). * Returns true (proceed) when no patterns are configured or none match. + * + * Matching rules: + * - Case-insensitive + * - The directive must be surrounded by whitespace or string boundaries, so + * "prefix[skip deploy]suffix" does NOT accidentally skip + * - Internal whitespace in the pattern is normalised: tokens are split on \s+ + * and rejoined with \s* in the regex, so "[skip deploy]" matches + * "[skip deploy]" and "skip-checks: true" matches "skip-checks:true" */ public function isValid($value): bool { @@ -21,13 +29,38 @@ class CommitSkipPatterns extends Validator return false; } - $patterns = $this->normalizePatterns($this->patterns); - if (empty($patterns)) { - return true; - } + foreach ($this->patterns as $pattern) { + if (!is_string($pattern)) { + continue; + } - foreach ($this->extractDirectives($value) as $directive) { - if (isset($patterns[$directive])) { + $pattern = trim($pattern); + if ($pattern === '') { + continue; + } + + // Split on whitespace; each token is regex-quoted. Tokens are rejoined + // with \s+ (required space) so that "skipappwrite" does NOT match the + // pattern "skip appwrite". The only exception: when the preceding token + // ends with ":" (git trailer style), \s* is used so that + // "skip-checks:true" still matches the pattern "skip-checks: true". + $tokens = preg_split('/\s+/', $pattern); + $regexParts = []; + $count = count($tokens); + for ($i = 0; $i < $count; $i++) { + $regexParts[] = preg_quote($tokens[$i], '~'); + if ($i < $count - 1) { + $regexParts[] = str_ends_with($tokens[$i], ':') ? '\s*' : '\s+'; + } + } + $regexBody = implode('', $regexParts); + + // (? $patterns - * @return array - */ - private function normalizePatterns(array $patterns): array - { - $normalized = []; - - foreach ($patterns as $pattern) { - if (!\is_string($pattern)) { - continue; - } - - $pattern = $this->normalizeDirective($pattern); - if ($pattern === '') { - continue; - } - - $normalized[$pattern] = true; - } - - return $normalized; - } - - /** - * @return array - */ - private function extractDirectives(string $message): array - { - $directives = []; - - if (\preg_match_all('/\[[^\]\r\n]+\]/u', $message, $matches) > 0) { - foreach ($matches[0] as $match) { - $directives[] = $this->normalizeDirective($match); - } - } - - foreach (\preg_split("/\r\n|\n|\r/", $message) ?: [] as $line) { - $line = \trim($line); - if ($line === '' || !\str_contains($line, ':')) { - continue; - } - - $directives[] = $this->normalizeDirective($line); - } - - return \array_values(\array_filter(\array_unique($directives))); - } - - private function normalizeDirective(string $value): string - { - $value = \trim($value); - if ($value === '') { - return ''; - } - - $value = (string) \preg_replace('/\s+/u', ' ', $value); - return \mb_strtolower($value); - } } diff --git a/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php index efc88bbf6a..b546199d22 100644 --- a/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php +++ b/tests/unit/Vcs/Validator/CommitSkipPatternsTest.php @@ -151,4 +151,65 @@ class CommitSkipPatternsTest extends TestCase $msg = "feat: add new stuff\n\nMore detail here.\n\nskip-checks:true"; $this->assertFalse($validator->isValid($msg)); } + + // ------------------------------------------------------------------------- + // Plain-word patterns: "skip appwrite" and "appwrite skip" + // ------------------------------------------------------------------------- + + public function testSkipAppwritePatternSkips(): void + { + $validator = new CommitSkipPatterns(['skip appwrite']); + $this->assertFalse($validator->isValid('docs: update readme skip appwrite')); + $this->assertFalse($validator->isValid('skip appwrite')); + } + + public function testSkipAppwritePatternCaseInsensitive(): void + { + $validator = new CommitSkipPatterns(['skip appwrite']); + $this->assertFalse($validator->isValid('SKIP APPWRITE')); + $this->assertFalse($validator->isValid('Skip Appwrite')); + $this->assertFalse($validator->isValid('SKIP appwrite')); + } + + public function testSkipAppwritePatternNoMatchProceeds(): void + { + $validator = new CommitSkipPatterns(['skip appwrite']); + $this->assertTrue($validator->isValid('feat: real feature')); + $this->assertTrue($validator->isValid('skipappwrite')); // no space — not standalone + $this->assertTrue($validator->isValid('appwrite is great')); + } + + public function testAppwriteSkipPatternSkips(): void + { + $validator = new CommitSkipPatterns(['appwrite skip']); + $this->assertFalse($validator->isValid('appwrite skip ci')); + $this->assertFalse($validator->isValid('docs appwrite skip')); + $this->assertFalse($validator->isValid('appwrite skip')); + } + + public function testAppwriteSkipPatternCaseInsensitive(): void + { + $validator = new CommitSkipPatterns(['appwrite skip']); + $this->assertFalse($validator->isValid('APPWRITE SKIP')); + $this->assertFalse($validator->isValid('Appwrite Skip')); + $this->assertFalse($validator->isValid('appwrite SKIP')); + } + + public function testAppwriteSkipPatternNoMatchProceeds(): void + { + $validator = new CommitSkipPatterns(['appwrite skip']); + $this->assertTrue($validator->isValid('feat: deploy appwrite changes')); + $this->assertTrue($validator->isValid('appwriteskip')); // no space — not standalone + $this->assertTrue($validator->isValid('skip the appwrite stuff')); + } + + public function testBothAppwritePatternsInArray(): void + { + $validator = new CommitSkipPatterns(['skip appwrite', 'appwrite skip']); + $this->assertFalse($validator->isValid('skip appwrite')); + $this->assertFalse($validator->isValid('appwrite skip')); + $this->assertFalse($validator->isValid('SKIP APPWRITE')); + $this->assertFalse($validator->isValid('APPWRITE SKIP')); + $this->assertTrue($validator->isValid('feat: deploy appwrite changes')); + } } From 06068c04066e41b91992c42e59156cb82c0dc701 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 12 May 2026 20:51:57 +0300 Subject: [PATCH 19/97] skip subQueryTargets --- app/controllers/api/users.php | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3f52069609..2251520a92 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -731,6 +731,13 @@ Http::get('/v1/users') $cursor->setValue($cursorDocument); } + $skipFilters = ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships']; + + $selects = Query::getByType($queries, [Query::TYPE_SELECT]); + if (empty($selects)) { + $skipFilters[] = 'subQueryTargets'; + } + $users = []; $total = 0; @@ -743,7 +750,28 @@ Http::get('/v1/users') } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - }, ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships']); + }, $skipFilters); + + if (empty($selects) && !empty($users)) { + $sequences = []; + foreach ($users as $user) { + $sequences[] = $user->getSequence(); + } + + $targets = $dbForProject->find('targets', [ + Query::equal('userInternalId', $sequences), + Query::limit(PHP_INT_MAX), + ]); + + $targetsByUser = []; + foreach ($targets as $target) { + $targetsByUser[$target->getAttribute('userInternalId')][] = $target; + } + + foreach ($users as $user) { + $user->setAttribute('targets', $targetsByUser[$user->getSequence()] ?? []); + } + } $response->dynamic(new Document([ 'users' => $users, From 2ea66a0d8ba1ab6ddedcddc0c6d85994aaba5165 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 May 2026 08:36:57 +0300 Subject: [PATCH 20/97] APP_LIMIT_SUBQUERY --- app/controllers/api/users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 2251520a92..38f1fb2159 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -760,7 +760,7 @@ Http::get('/v1/users') $targets = $dbForProject->find('targets', [ Query::equal('userInternalId', $sequences), - Query::limit(PHP_INT_MAX), + Query::limit(\count($sequences) * APP_LIMIT_SUBQUERY), ]); $targetsByUser = []; From 1435e2c526f8472df6ecc622c3d840278b38a9bc Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 May 2026 08:41:14 +0300 Subject: [PATCH 21/97] use max --- app/controllers/api/users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 38f1fb2159..2251520a92 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -760,7 +760,7 @@ Http::get('/v1/users') $targets = $dbForProject->find('targets', [ Query::equal('userInternalId', $sequences), - Query::limit(\count($sequences) * APP_LIMIT_SUBQUERY), + Query::limit(PHP_INT_MAX), ]); $targetsByUser = []; From 8b4c90e6033f288f6cc7c25c2d465775e02adc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 11:47:46 +0200 Subject: [PATCH 22/97] Introduce organization endpoints --- src/Appwrite/Platform/Appwrite.php | 2 + .../Organization/Http/Projects/Action.php | 11 + .../Organization/Http/Projects/Create.php | 238 ++++++++++++++++++ .../Organization/Http/Projects/Get.php | 79 ++++++ .../Organization/Http/Projects/Update.php | 100 ++++++++ .../Organization/Http/Projects/XList.php | 201 +++++++++++++++ .../Platform/Modules/Organization/Module.php | 14 ++ .../Modules/Organization/Services/Http.php | 22 ++ 8 files changed, 667 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Module.php create mode 100644 src/Appwrite/Platform/Modules/Organization/Services/Http.php diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index a9cd1a8e2f..e41d508522 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -11,6 +11,7 @@ use Appwrite\Platform\Modules\Databases; use Appwrite\Platform\Modules\Functions; use Appwrite\Platform\Modules\Health; use Appwrite\Platform\Modules\Migrations; +use Appwrite\Platform\Modules\Organization; use Appwrite\Platform\Modules\Project; use Appwrite\Platform\Modules\Projects; use Appwrite\Platform\Modules\Proxy; @@ -42,6 +43,7 @@ class Appwrite extends Platform $this->addModule(new VCS\Module()); $this->addModule(new Webhooks\Module()); $this->addModule(new Migrations\Module()); + $this->addModule(new Organization\Module()); $this->addModule(new Project\Module()); $this->addModule(new Advisor\Module()); } diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php new file mode 100644 index 0000000000..0160e2aa04 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php @@ -0,0 +1,11 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/organizations/:organizationId/projects') + ->desc('Create project') + ->groups(['api', 'projects']) + ->label('audits.event', 'projects.create') + ->label('audits.resource', 'project/{response.$id}') + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'create', + description: '/docs/references/projects/create.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('organizationId', '', new UID(), 'Organization unique ID.') + ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->inject('request') + ->inject('response') + ->inject('dbForPlatform') + ->inject('cache') + ->inject('pools') + ->inject('hooks') + ->callback($this->action(...)); + } + + public function action(string $organizationId, string $projectId, string $name, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) + { + $team = $dbForPlatform->getDocument('teams', $organizationId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); + + if (!empty($allowList) && !\in_array($region, $allowList)) { + throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported'); + } + + $auth = Config::getParam('auth', []); + $auths = [ + 'limit' => 0, + 'maxSessions' => 0, + 'passwordHistory' => 0, + 'passwordDictionary' => false, + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, + 'personalDataCheck' => false, + 'disposableEmails' => false, + 'canonicalEmails' => false, + 'freeEmails' => false, + 'mockNumbers' => [], + 'sessionAlerts' => false, + 'membershipsUserName' => false, + 'membershipsUserEmail' => false, + 'membershipsMfa' => false, + 'membershipsUserId' => false, + 'membershipsUserPhone' => false, + 'invalidateSessions' => true + ]; + + foreach ($auth as $method) { + $auths[$method['key'] ?? ''] = true; + } + + $projectId = ($projectId == 'unique()') ? ID::unique() : $projectId; + + if ($projectId === 'console') { + throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); + } + + $databases = Config::getParam('pools-database', []); + + if ($region !== 'default') { + $databaseKeys = System::getEnv('_APP_DATABASE_KEYS', ''); + $keys = explode(',', $databaseKeys); + $databases = array_filter($keys, function ($value) use ($region) { + return str_contains($value, $region); + }); + } + + $databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE'); + $index = \array_search($databaseOverride, $databases); + if ($index !== false) { + $dsn = $databases[$index]; + } else { + $dsn = $databases[array_rand($databases)]; + } + + // TODO: Temporary until all projects are using shared tables. + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn, $sharedTables)) { + $schema = 'appwrite'; + $database = 'appwrite'; + $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''); + $dsn = $schema . '://' . $dsn . '?database=' . $database; + + if (!empty($namespace)) { + $dsn .= '&namespace=' . $namespace; + } + } + + try { + $project = $dbForPlatform->createDocument('projects', new Document([ + '$id' => $projectId, + '$permissions' => $this->getPermissions($organizationId, $projectId), + 'name' => $name, + 'teamInternalId' => $team->getSequence(), + 'teamId' => $team->getId(), + 'region' => $region, + 'version' => APP_VERSION_STABLE, + 'services' => new \stdClass(), + 'platforms' => null, + 'oAuthProviders' => [], + 'webhooks' => null, + 'keys' => null, + 'auths' => $auths, + 'accessedAt' => DateTime::now(), + 'search' => implode(' ', [$projectId, $name]), + 'database' => $dsn, + 'labels' => [], + 'status' => PROJECT_STATUS_ACTIVE, + ])); + } catch (Duplicate) { + throw new Exception(Exception::PROJECT_ALREADY_EXISTS); + } + + try { + $dsn = new DSN($dsn); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $dsn); + } + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $projectTables = !\in_array($dsn->getHost(), $sharedTables); + + if ($projectTables) { + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $dbForProject = new Database($adapter, $cache); + $dbForProject + ->setDatabase(APP_DATABASE) + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + + $create = true; + + try { + $dbForProject->create(); + } catch (Duplicate) { + $create = false; + } + + $adapter = new AdapterDatabase($dbForProject); + $audit = new Audit($adapter); + $audit->setup(); + + if ($create) { + /** @var array $collections */ + $collections = Config::getParam('collections', [])['projects'] ?? []; + + foreach ($collections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + + $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); + $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); + + try { + $dbForProject->createCollection($key, $attributes, $indexes); + } catch (Duplicate) { + // Collection already exists + } + } + } + } + + // Hook allowing instant project mirroring during migration + // Outside of migration, hook is not registered and has no effect + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php new file mode 100644 index 0000000000..012f223ae5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php @@ -0,0 +1,79 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') + ->desc('Get project') + ->groups(['api', 'project']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'project', + group: null, + name: 'get', + description: <<param('organizationId', '', new UID(), 'Organization unique ID.') + ->param('projectId', '', new UID(), 'Project unique ID.') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $organizationId, + string $projectId, + Response $response, + Database $dbForPlatform, + ) { + $team = $dbForPlatform->getDocument('teams', $organizationId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $response->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php new file mode 100644 index 0000000000..883e8e4626 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -0,0 +1,100 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') + ->desc('Update project') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('audits.event', 'projects.update') + ->label('audits.resource', 'project/{request.projectId}') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'update', + description: '/docs/references/projects/update.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('organizationId', '', new UID(), 'Organization unique ID.') + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) + ->param('logo', '', new Text(1024), 'Project logo.', true) + ->param('url', '', new URL(), 'Project URL.', true) + ->param('legalName', '', new Text(256), 'Project legal name. Max length: 256 chars.', true) + ->param('legalCountry', '', new Text(256), 'Project legal country. Max length: 256 chars.', true) + ->param('legalState', '', new Text(256), 'Project legal state. Max length: 256 chars.', true) + ->param('legalCity', '', new Text(256), 'Project legal city. Max length: 256 chars.', true) + ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) + ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action(string $organizationId, string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) + { + $team = $dbForPlatform->getDocument('teams', $organizationId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'name' => $name, + 'description' => $description, + 'logo' => $logo, + 'url' => $url, + 'legalName' => $legalName, + 'legalCountry' => $legalCountry, + 'legalState' => $legalState, + 'legalCity' => $legalCity, + 'legalAddress' => $legalAddress, + 'legalTaxId' => $legalTaxId, + 'search' => implode(' ', [$projectId, $name]), + ])); + + $response->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php new file mode 100644 index 0000000000..cd2c194289 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -0,0 +1,201 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/organizations/:organizationId/projects') + ->desc('List projects') + ->groups(['api', 'projects']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'list', + description: <<param('organizationId', '', new UID(), 'Organization unique ID.') + ->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action(string $organizationId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform) + { + $team = $dbForPlatform->getDocument('teams', $organizationId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + $queries[] = Query::equal('teamInternalId', [$team->getSequence()]); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $projectId = $cursor->getValue(); + $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + try { + $selectQueries = Query::groupByType($queries)['selections']; + $filterQueries = Query::groupByType($queries)['filters']; + + $projects = $this->find($dbForPlatform, $queries, $selectQueries); + $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (Order $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + + $response->addFilter(new ListSelection($selectQueries, 'projects')); + + $response->dynamic(new Document([ + 'projects' => $projects, + 'total' => $total, + ]), Response::MODEL_PROJECT_LIST); + } + + // Build mapping of columns to their subQuery filters + private static function getAttributeToSubQueryFilters(): array + { + if (self::$attributeToSubQueryFilters !== null) { + return self::$attributeToSubQueryFilters; + } + + self::$attributeToSubQueryFilters = []; + + $collections = Config::getParam('collections', []); + $projectAttributes = $collections['platform']['projects']['attributes'] ?? []; + + foreach ($projectAttributes as $attribute) { + $attributeId = $attribute['$id'] ?? null; + $filters = $attribute['filters'] ?? []; + + if ($attributeId === null || empty($filters)) { + continue; + } + + // extract only subQuery filters + $subQueryFilters = \array_filter($filters, function ($filter) { + return \str_starts_with($filter, 'subQuery'); + }); + + if (!empty($subQueryFilters)) { + self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters); + } + } + + return self::$attributeToSubQueryFilters; + } + + private function find(Database $dbForPlatform, array $queries, array $selectQueries): array + { + if (empty($selectQueries)) { + return $dbForPlatform->find('projects', $queries); + } + + $selectedAttributes = []; + foreach ($selectQueries as $query) { + foreach ($query->getValues() as $value) { + $selectedAttributes[] = $value; + } + } + + if (\in_array('*', $selectedAttributes)) { + return $dbForPlatform->find('projects', $queries); + } + + $filtersToSkipMap = []; + $selectedAttributesMap = \array_flip($selectedAttributes); + $attributeToSubQueryFilters = self::getAttributeToSubQueryFilters(); + + foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) { + if (!isset($selectedAttributesMap[$attributeName])) { + foreach ($subQueryFilters as $filter) { + $filtersToSkipMap[$filter] = true; + } + } + } + + $filtersToSkip = \array_keys($filtersToSkipMap); + + return empty($filtersToSkip) + ? $dbForPlatform->find('projects', $queries) + : $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Module.php b/src/Appwrite/Platform/Modules/Organization/Module.php new file mode 100644 index 0000000000..eb7a2dc433 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Services/Http.php b/src/Appwrite/Platform/Modules/Organization/Services/Http.php new file mode 100644 index 0000000000..030bff9fac --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Services/Http.php @@ -0,0 +1,22 @@ +type = Service::TYPE_HTTP; + + $this->addAction(CreateProject::getName(), new CreateProject()); + $this->addAction(ListProjects::getName(), new ListProjects()); + $this->addAction(GetProject::getName(), new GetProject()); + $this->addAction(UpdateProject::getName(), new UpdateProject()); + } +} From 275a6fe078b8fafec4c5655f4cbdecb5c1ba6199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 12:01:51 +0200 Subject: [PATCH 23/97] merge endpoints as aliases --- .../Organization/Http/Projects/Create.php | 11 +- .../Organization/Http/Projects/Update.php | 11 +- .../Organization/Http/Projects/XList.php | 11 +- .../Modules/Projects/Http/Projects/Create.php | 245 ------------------ .../Modules/Projects/Http/Projects/Update.php | 94 ------- .../Modules/Projects/Http/Projects/XList.php | 197 -------------- .../Modules/Projects/Services/Http.php | 6 - 7 files changed, 30 insertions(+), 545 deletions(-) delete mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php delete mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php delete mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 6ad8a25e9f..aebf228380 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -42,6 +42,7 @@ class Create extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/organizations/:organizationId/projects') + ->httpAlias('/v1/projects') ->desc('Create project') ->groups(['api', 'projects']) ->label('audits.event', 'projects.create') @@ -70,11 +71,19 @@ class Create extends Action ->inject('cache') ->inject('pools') ->inject('hooks') + ->inject('team') ->callback($this->action(...)); } - public function action(string $organizationId, string $projectId, string $name, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) + public function action(string $organizationId, string $projectId, string $name, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) { + if (empty($organizationId)) { + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + $organizationId = $team->getId(); + } + $team = $dbForPlatform->getDocument('teams', $organizationId); if ($team->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index 883e8e4626..36009a9512 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -28,6 +28,7 @@ class Update extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') + ->httpAlias('/v1/projects/:projectId') ->desc('Update project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -60,11 +61,19 @@ class Update extends Action ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) ->inject('response') ->inject('dbForPlatform') + ->inject('team') ->callback($this->action(...)); } - public function action(string $organizationId, string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) + public function action(string $organizationId, string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform, Document $team) { + if (empty($organizationId)) { + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + $organizationId = $team->getId(); + } + $team = $dbForPlatform->getDocument('teams', $organizationId); if ($team->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index cd2c194289..837cbff9e5 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -45,6 +45,7 @@ class XList extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/organizations/:organizationId/projects') + ->httpAlias('/v1/projects') ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -70,11 +71,19 @@ class XList extends Action ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForPlatform') + ->inject('team') ->callback($this->action(...)); } - public function action(string $organizationId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform) + public function action(string $organizationId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) { + if (empty($organizationId)) { + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + $organizationId = $team->getId(); + } + $team = $dbForPlatform->getDocument('teams', $organizationId); if ($team->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php deleted file mode 100644 index d2c92fc65c..0000000000 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ /dev/null @@ -1,245 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/projects') - ->desc('Create project') - ->groups(['api', 'projects']) - ->label('audits.event', 'projects.create') - ->label('audits.resource', 'project/{response.$id}') - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'create', - description: '/docs/references/projects/create.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_PROJECT, - ) - ] - )) - ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') - ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') - ->param('teamId', '', new UID(), 'Team unique ID.') - ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) - ->inject('request') - ->inject('response') - ->inject('dbForPlatform') - ->inject('cache') - ->inject('pools') - ->inject('hooks') - ->callback($this->action(...)); - } - - public function action(string $projectId, string $name, string $teamId, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) - { - $team = $dbForPlatform->getDocument('teams', $teamId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - - $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); - - if (!empty($allowList) && !\in_array($region, $allowList)) { - throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported'); - } - - $auth = Config::getParam('auth', []); - $auths = [ - 'limit' => 0, - 'maxSessions' => 0, - 'passwordHistory' => 0, - 'passwordDictionary' => false, - 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, - 'personalDataCheck' => false, - 'disposableEmails' => false, - 'canonicalEmails' => false, - 'freeEmails' => false, - 'mockNumbers' => [], - 'sessionAlerts' => false, - 'membershipsUserName' => false, - 'membershipsUserEmail' => false, - 'membershipsMfa' => false, - 'membershipsUserId' => false, - 'membershipsUserPhone' => false, - 'invalidateSessions' => true - ]; - - foreach ($auth as $method) { - $auths[$method['key'] ?? ''] = true; - } - - $projectId = ($projectId == 'unique()') ? ID::unique() : $projectId; - - if ($projectId === 'console') { - throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); - } - - $databases = Config::getParam('pools-database', []); - - if ($region !== 'default') { - $databaseKeys = System::getEnv('_APP_DATABASE_KEYS', ''); - $keys = explode(',', $databaseKeys); - $databases = array_filter($keys, function ($value) use ($region) { - return str_contains($value, $region); - }); - } - - $databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE'); - $index = \array_search($databaseOverride, $databases); - if ($index !== false) { - $dsn = $databases[$index]; - } else { - $dsn = $databases[array_rand($databases)]; - } - - // TODO: Temporary until all projects are using shared tables. - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - - if (\in_array($dsn, $sharedTables)) { - $schema = 'appwrite'; - $database = 'appwrite'; - $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''); - $dsn = $schema . '://' . $dsn . '?database=' . $database; - - if (!empty($namespace)) { - $dsn .= '&namespace=' . $namespace; - } - } - - try { - $project = $dbForPlatform->createDocument('projects', new Document([ - '$id' => $projectId, - '$permissions' => $this->getPermissions($teamId, $projectId), - 'name' => $name, - 'teamInternalId' => $team->getSequence(), - 'teamId' => $team->getId(), - 'region' => $region, - 'version' => APP_VERSION_STABLE, - 'services' => new \stdClass(), - 'platforms' => null, - 'oAuthProviders' => [], - 'webhooks' => null, - 'keys' => null, - 'auths' => $auths, - 'accessedAt' => DateTime::now(), - 'search' => implode(' ', [$projectId, $name]), - 'database' => $dsn, - 'labels' => [], - 'status' => PROJECT_STATUS_ACTIVE, - ])); - } catch (Duplicate) { - throw new Exception(Exception::PROJECT_ALREADY_EXISTS); - } - - try { - $dsn = new DSN($dsn); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $dsn); - } - - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $projectTables = !\in_array($dsn->getHost(), $sharedTables); - - if ($projectTables) { - $adapter = new DatabasePool($pools->get($dsn->getHost())); - $dbForProject = new Database($adapter, $cache); - $dbForProject - ->setDatabase(APP_DATABASE) - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - - $create = true; - - try { - $dbForProject->create(); - } catch (Duplicate) { - $create = false; - } - - $adapter = new AdapterDatabase($dbForProject); - $audit = new Audit($adapter); - $audit->setup(); - - if ($create) { - /** @var array $collections */ - $collections = Config::getParam('collections', [])['projects'] ?? []; - - foreach ($collections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - - $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); - $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); - - try { - $dbForProject->createCollection($key, $attributes, $indexes); - } catch (Duplicate) { - // Collection already exists - } - } - } - } - - // Hook allowing instant project mirroring during migration - // Outside of migration, hook is not registered and has no effect - $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($project, Response::MODEL_PROJECT); - } -} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php deleted file mode 100644 index 29c26b33ea..0000000000 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php +++ /dev/null @@ -1,94 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/projects/:projectId') - ->desc('Update project') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('audits.event', 'projects.update') - ->label('audits.resource', 'project/{request.projectId}') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'update', - description: '/docs/references/projects/update.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) - ->param('projectId', '', new UID(), 'Project unique ID.') - ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') - ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) - ->param('logo', '', new Text(1024), 'Project logo.', true) - ->param('url', '', new URL(), 'Project URL.', true) - ->param('legalName', '', new Text(256), 'Project legal name. Max length: 256 chars.', true) - ->param('legalCountry', '', new Text(256), 'Project legal country. Max length: 256 chars.', true) - ->param('legalState', '', new Text(256), 'Project legal state. Max length: 256 chars.', true) - ->param('legalCity', '', new Text(256), 'Project legal city. Max length: 256 chars.', true) - ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) - ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->callback($this->action(...)); - } - - public function action(string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) - { - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project - ->setAttribute('name', $name) - ->setAttribute('description', $description) - ->setAttribute('logo', $logo) - ->setAttribute('url', $url) - ->setAttribute('legalName', $legalName) - ->setAttribute('legalCountry', $legalCountry) - ->setAttribute('legalState', $legalState) - ->setAttribute('legalCity', $legalCity) - ->setAttribute('legalAddress', $legalAddress) - ->setAttribute('legalTaxId', $legalTaxId) - ->setAttribute('search', implode(' ', [$projectId, $name]))); - - $response->dynamic($project, Response::MODEL_PROJECT); - } -} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php deleted file mode 100644 index 0d2a951388..0000000000 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ /dev/null @@ -1,197 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/projects') - ->desc('List projects') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'list', - description: <<param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) - ->inject('response') - ->inject('dbForPlatform') - ->inject('team') - ->callback($this->action(...)); - } - - public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) - { - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - if (!empty($search)) { - $queries[] = Query::search('search', $search); - } - - if (!$team->isEmpty()) { - $queries[] = Query::equal('teamInternalId', [$team->getSequence()]); - } - - $cursor = Query::getCursorQueries($queries, false); - $cursor = \reset($cursor); - - if ($cursor !== false) { - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $projectId = $cursor->getValue(); - $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - try { - $selectQueries = Query::groupByType($queries)['selections']; - $filterQueries = Query::groupByType($queries)['filters']; - - $projects = $this->find($dbForPlatform, $queries, $selectQueries); - $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; - } catch (Order $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); - } - - $response->addFilter(new ListSelection($selectQueries, 'projects')); - - $response->dynamic(new Document([ - 'projects' => $projects, - 'total' => $total, - ]), Response::MODEL_PROJECT_LIST); - } - - // Build mapping of columns to their subQuery filters - private static function getAttributeToSubQueryFilters(): array - { - if (self::$attributeToSubQueryFilters !== null) { - return self::$attributeToSubQueryFilters; - } - - self::$attributeToSubQueryFilters = []; - - $collections = Config::getParam('collections', []); - $projectAttributes = $collections['platform']['projects']['attributes'] ?? []; - - foreach ($projectAttributes as $attribute) { - $attributeId = $attribute['$id'] ?? null; - $filters = $attribute['filters'] ?? []; - - if ($attributeId === null || empty($filters)) { - continue; - } - - // extract only subQuery filters - $subQueryFilters = \array_filter($filters, function ($filter) { - return \str_starts_with($filter, 'subQuery'); - }); - - if (!empty($subQueryFilters)) { - self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters); - } - } - - return self::$attributeToSubQueryFilters; - } - - private function find(Database $dbForPlatform, array $queries, array $selectQueries): array - { - if (empty($selectQueries)) { - return $dbForPlatform->find('projects', $queries); - } - - $selectedAttributes = []; - foreach ($selectQueries as $query) { - foreach ($query->getValues() as $value) { - $selectedAttributes[] = $value; - } - } - - if (\in_array('*', $selectedAttributes)) { - return $dbForPlatform->find('projects', $queries); - } - - $filtersToSkipMap = []; - $selectedAttributesMap = \array_flip($selectedAttributes); - $attributeToSubQueryFilters = self::getAttributeToSubQueryFilters(); - - foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) { - if (!isset($selectedAttributesMap[$attributeName])) { - foreach ($subQueryFilters as $filter) { - $filtersToSkipMap[$filter] = true; - } - } - } - - $filtersToSkip = \array_keys($filtersToSkipMap); - - return empty($filtersToSkip) - ? $dbForPlatform->find('projects', $queries) - : $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip); - } -} diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index 8275e664d5..b1662cef31 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -7,10 +7,7 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; -use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject; use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam; -use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject; -use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; use Appwrite\Platform\Modules\Projects\Http\Schedules\Create as CreateSchedule; use Appwrite\Platform\Modules\Projects\Http\Schedules\Get as GetSchedule; use Appwrite\Platform\Modules\Projects\Http\Schedules\XList as ListSchedules; @@ -27,9 +24,6 @@ class Http extends Service $this->addAction(ListDevKeys::getName(), new ListDevKeys()); $this->addAction(DeleteDevKey::getName(), new DeleteDevKey()); - $this->addAction(CreateProject::getName(), new CreateProject()); - $this->addAction(UpdateProject::getName(), new UpdateProject()); - $this->addAction(ListProjects::getName(), new ListProjects()); $this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam()); $this->addAction(CreateSchedule::getName(), new CreateSchedule()); From b524b9f9340caa35f2bc337a9ba573df0d0a134b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 12:18:09 +0200 Subject: [PATCH 24/97] Fix scopes --- app/config/roles.php | 2 ++ app/config/scopes/organization.php | 4 ++-- .../Platform/Modules/Organization/Http/Projects/Create.php | 2 +- .../Platform/Modules/Organization/Http/Projects/Get.php | 2 +- .../Platform/Modules/Organization/Http/Projects/Update.php | 2 +- .../Platform/Modules/Organization/Http/Projects/XList.php | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index cb4b178a29..fd3b9d887d 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -19,6 +19,7 @@ $member = [ 'files.read', 'files.write', 'projects.read', + 'organization.projects.read', 'locale.read', 'avatars.read', 'executions.read', @@ -64,6 +65,7 @@ $admins = [ 'templates.read', 'templates.write', 'projects.write', + 'organization.projects.write', 'keys.read', 'keys.write', 'devKeys.read', diff --git a/app/config/scopes/organization.php b/app/config/scopes/organization.php index 228a1437f2..f531975353 100644 --- a/app/config/scopes/organization.php +++ b/app/config/scopes/organization.php @@ -3,10 +3,10 @@ // List of scopes for organization (teams) API keys return [ - "projects.read" => [ + "organization.projects.read" => [ "description" => 'Access to read organization\'s projects', ], - "projects.write" => [ + "organization.projects.write" => [ "description" => "Access to create, update, and delete projects in organization", ], diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index aebf228380..f7cc3c4203 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -47,7 +47,7 @@ class Create extends Action ->groups(['api', 'projects']) ->label('audits.event', 'projects.create') ->label('audits.resource', 'project/{response.$id}') - ->label('scope', 'projects.write') + ->label('scope', 'organization.projects.write') ->label('sdk', new Method( namespace: 'projects', group: 'projects', diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php index 012f223ae5..333d8711d6 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php @@ -28,7 +28,7 @@ class Get extends Action ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') ->desc('Get project') ->groups(['api', 'project']) - ->label('scope', 'projects.read') + ->label('scope', 'organization.projects.read') ->label('sdk', new Method( namespace: 'project', group: null, diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index 36009a9512..a21921413e 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId') ->desc('Update project') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'organization.projects.write') ->label('audits.event', 'projects.update') ->label('audits.resource', 'project/{request.projectId}') ->label('sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index 837cbff9e5..646b6ec968 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -48,7 +48,7 @@ class XList extends Action ->httpAlias('/v1/projects') ->desc('List projects') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'organization.projects.read') ->label('sdk', new Method( namespace: 'projects', group: 'projects', From 8a4a6d83d402fa8d33a9eb1e0927e2b7008e3ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 13:06:39 +0200 Subject: [PATCH 25/97] Manual migration fixes --- .../Modules/Organization/Http/Init.php | 28 ++++++++++ .../Organization/Http/Projects/Create.php | 36 ++++--------- .../Organization/Http/Projects/Get.php | 23 ++++----- .../Organization/Http/Projects/Update.php | 51 ++++--------------- .../Organization/Http/Projects/XList.php | 29 +++-------- .../Modules/Organization/Services/Http.php | 5 ++ 6 files changed, 70 insertions(+), 102 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Init.php diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Init.php b/src/Appwrite/Platform/Modules/Organization/Http/Init.php new file mode 100644 index 0000000000..56eb6db3a0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Init.php @@ -0,0 +1,28 @@ +setType(Action::TYPE_INIT) + ->groups(['organization']) + ->inject('team') + ->callback(function (Document $team) { + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + }); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index f7cc3c4203..5f4575a300 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -20,7 +20,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; -use Utopia\Database\Validator\UID; use Utopia\DSN\DSN; use Utopia\Platform\Scope\HTTP; use Utopia\Pools\Group; @@ -41,19 +40,21 @@ class Create extends Action { $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/organizations/:organizationId/projects') + ->setHttpPath('/v1/organization/projects') ->httpAlias('/v1/projects') - ->desc('Create project') - ->groups(['api', 'projects']) + ->desc('Create organization project') + ->groups(['api', 'organization']) ->label('audits.event', 'projects.create') ->label('audits.resource', 'project/{response.$id}') ->label('scope', 'organization.projects.write') ->label('sdk', new Method( - namespace: 'projects', + namespace: 'organization', group: 'projects', - name: 'create', - description: '/docs/references/projects/create.md', - auth: [AuthType::ADMIN], + name: 'createProject', + description: <<param('organizationId', '', new UID(), 'Organization unique ID.') ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) - ->inject('request') ->inject('response') ->inject('dbForPlatform') ->inject('cache') @@ -75,21 +74,8 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $organizationId, string $projectId, string $name, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) + public function action(string $projectId, string $name, string $region, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) { - if (empty($organizationId)) { - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $organizationId = $team->getId(); - } - - $team = $dbForPlatform->getDocument('teams', $organizationId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); if (!empty($allowList) && !\in_array($region, $allowList)) { @@ -162,7 +148,7 @@ class Create extends Action try { $project = $dbForPlatform->createDocument('projects', new Document([ '$id' => $projectId, - '$permissions' => $this->getPermissions($organizationId, $projectId), + '$permissions' => $this->getPermissions($team->getId(), $projectId), 'name' => $name, 'teamInternalId' => $team->getSequence(), 'teamId' => $team->getId(), diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php index 333d8711d6..d75356704d 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.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\UID; use Utopia\Platform\Scope\HTTP; @@ -25,14 +26,14 @@ class Get extends Action { $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') - ->desc('Get project') - ->groups(['api', 'project']) + ->setHttpPath('/v1/organization/projects/:projectId') + ->desc('Get organization project') + ->groups(['api', 'organization']) ->label('scope', 'organization.projects.read') ->label('sdk', new Method( - namespace: 'project', - group: null, - name: 'get', + namespace: 'organization', + group: 'projects', + name: 'getProject', description: <<param('organizationId', '', new UID(), 'Organization unique ID.') ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') ->inject('dbForPlatform') + ->inject('team') ->callback($this->action(...)); } public function action( - string $organizationId, string $projectId, Response $response, Database $dbForPlatform, + Document $team, ) { - $team = $dbForPlatform->getDocument('teams', $organizationId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index a21921413e..1cb70a6aaf 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -12,7 +12,6 @@ use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Text; -use Utopia\Validator\URL; class Update extends Action { @@ -27,19 +26,21 @@ class Update extends Action { $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/organizations/:organizationId/projects/:projectId') + ->setHttpPath('/v1/organization/projects/:projectId') ->httpAlias('/v1/projects/:projectId') - ->desc('Update project') - ->groups(['api', 'projects']) + ->desc('Update organization project') + ->groups(['api', 'organization']) ->label('scope', 'organization.projects.write') ->label('audits.event', 'projects.update') ->label('audits.resource', 'project/{request.projectId}') ->label('sdk', new Method( - namespace: 'projects', + namespace: 'organization', group: 'projects', - name: 'update', - description: '/docs/references/projects/update.md', - auth: [AuthType::ADMIN], + name: 'updateProject', + description: <<param('organizationId', '', new UID(), 'Organization unique ID.') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') - ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) - ->param('logo', '', new Text(1024), 'Project logo.', true) - ->param('url', '', new URL(), 'Project URL.', true) - ->param('legalName', '', new Text(256), 'Project legal name. Max length: 256 chars.', true) - ->param('legalCountry', '', new Text(256), 'Project legal country. Max length: 256 chars.', true) - ->param('legalState', '', new Text(256), 'Project legal state. Max length: 256 chars.', true) - ->param('legalCity', '', new Text(256), 'Project legal city. Max length: 256 chars.', true) - ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) - ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) ->inject('response') ->inject('dbForPlatform') ->inject('team') ->callback($this->action(...)); } - public function action(string $organizationId, string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform, Document $team) + public function action(string $projectId, string $name, Response $response, Database $dbForPlatform, Document $team) { - if (empty($organizationId)) { - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $organizationId = $team->getId(); - } - - $team = $dbForPlatform->getDocument('teams', $organizationId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -92,15 +70,6 @@ class Update extends Action $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ 'name' => $name, - 'description' => $description, - 'logo' => $logo, - 'url' => $url, - 'legalName' => $legalName, - 'legalCountry' => $legalCountry, - 'legalState' => $legalState, - 'legalCity' => $legalCity, - 'legalAddress' => $legalAddress, - 'legalTaxId' => $legalTaxId, 'search' => implode(' ', [$projectId, $name]), ])); diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index 646b6ec968..2c2b480ef5 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -17,7 +17,6 @@ use Utopia\Database\Exception\Order; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; -use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator; use Utopia\Validator\Boolean; @@ -44,19 +43,19 @@ class XList extends Action { $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/organizations/:organizationId/projects') + ->setHttpPath('/v1/organization/projects') ->httpAlias('/v1/projects') - ->desc('List projects') - ->groups(['api', 'projects']) + ->desc('List organization projects') + ->groups(['api', 'organization']) ->label('scope', 'organization.projects.read') ->label('sdk', new Method( - namespace: 'projects', + namespace: 'organization', group: 'projects', - name: 'list', + name: 'listProjects', description: <<param('organizationId', '', new UID(), 'Organization unique ID.') ->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) @@ -75,21 +73,8 @@ class XList extends Action ->callback($this->action(...)); } - public function action(string $organizationId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) + public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) { - if (empty($organizationId)) { - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $organizationId = $team->getId(); - } - - $team = $dbForPlatform->getDocument('teams', $organizationId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { diff --git a/src/Appwrite/Platform/Modules/Organization/Services/Http.php b/src/Appwrite/Platform/Modules/Organization/Services/Http.php index 030bff9fac..4a7cd69220 100644 --- a/src/Appwrite/Platform/Modules/Organization/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Organization/Services/Http.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Organization\Services; +use Appwrite\Platform\Modules\Organization\Http\Init as Init; use Appwrite\Platform\Modules\Organization\Http\Projects\Create as CreateProject; use Appwrite\Platform\Modules\Organization\Http\Projects\Get as GetProject; use Appwrite\Platform\Modules\Organization\Http\Projects\Update as UpdateProject; @@ -14,6 +15,10 @@ class Http extends Service { $this->type = Service::TYPE_HTTP; + // Init hook + $this->addAction(Init::getName(), new Init()); + + // Projects $this->addAction(CreateProject::getName(), new CreateProject()); $this->addAction(ListProjects::getName(), new ListProjects()); $this->addAction(GetProject::getName(), new GetProject()); From d9446fa8926bc1743940dbd65868ecf369d5501b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 14 May 2026 14:45:00 +0300 Subject: [PATCH 26/97] address --- app/controllers/api/users.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 2251520a92..9230ae0190 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -758,10 +758,14 @@ Http::get('/v1/users') $sequences[] = $user->getSequence(); } - $targets = $dbForProject->find('targets', [ - Query::equal('userInternalId', $sequences), - Query::limit(PHP_INT_MAX), - ]); + try { + $targets = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->find('targets', [ + Query::equal('userInternalId', $sequences), + Query::limit(PHP_INT_MAX), + ])); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } $targetsByUser = []; foreach ($targets as $target) { From 94274381ff9b36db54474fccf77977e919b7a949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 13:52:25 +0200 Subject: [PATCH 27/97] agentic migration fixes --- .../Modules/Organization/Http/Projects/Create.php | 4 +++- .../Modules/Organization/Http/Projects/Get.php | 4 +++- .../Modules/Organization/Http/Projects/Update.php | 10 +++++++--- .../Modules/Organization/Http/Projects/XList.php | 10 ++++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 5f4575a300..9962289343 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Organization\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\ProjectId; @@ -60,7 +61,8 @@ class Create extends Action code: Response::STATUS_CODE_CREATED, model: Response::MODEL_PROJECT, ) - ] + ], + contentType: ContentType::JSON )) ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php index d75356704d..a85eb8fb15 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php @@ -69,6 +69,8 @@ class Get extends Action throw new Exception(Exception::PROJECT_NOT_FOUND); } - $response->dynamic($project, Response::MODEL_PROJECT); + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($project, Response::MODEL_PROJECT); } } diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index 1cb70a6aaf..9415374044 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Organization\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; @@ -32,7 +33,7 @@ class Update extends Action ->groups(['api', 'organization']) ->label('scope', 'organization.projects.write') ->label('audits.event', 'projects.update') - ->label('audits.resource', 'project/{request.projectId}') + ->label('audits.resource', 'project/{response.$id}') ->label('sdk', new Method( namespace: 'organization', group: 'projects', @@ -46,7 +47,8 @@ class Update extends Action code: Response::STATUS_CODE_OK, model: Response::MODEL_PROJECT, ) - ] + ], + contentType: ContentType::JSON )) ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') @@ -73,6 +75,8 @@ class Update extends Action 'search' => implode(' ', [$projectId, $name]), ])); - $response->dynamic($project, Response::MODEL_PROJECT); + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($project, Response::MODEL_PROJECT); } } diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index 2c2b480ef5..6f04222a69 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -118,10 +118,12 @@ class XList extends Action $response->addFilter(new ListSelection($selectQueries, 'projects')); - $response->dynamic(new Document([ - 'projects' => $projects, - 'total' => $total, - ]), Response::MODEL_PROJECT_LIST); + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic(new Document([ + 'projects' => $projects, + 'total' => $total, + ]), Response::MODEL_PROJECT_LIST); } // Build mapping of columns to their subQuery filters From aaa870e65637f816a46d5b44f23741e1d9468c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 13:52:34 +0200 Subject: [PATCH 28/97] Formatting fixes --- .../Platform/Modules/Organization/Http/Projects/Create.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 9962289343..d6f12c4f41 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -9,7 +9,6 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\ProjectId; -use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Audit\Adapter\Database as AdapterDatabase; use Utopia\Audit\Audit; From b618bf13538fb975c5da10aaa17c5601504c2acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 17:26:08 +0200 Subject: [PATCH 29/97] Fix inability to CRUD projects --- app/init/resources/request.php | 57 ++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 68f5968519..68a8af3605 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1,5 +1,6 @@ set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { + $context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization, Document $user) { $teamInternalId = ''; if ($project->getId() !== 'console') { $teamInternalId = $project->getAttribute('teamInternalId', ''); @@ -1100,7 +1103,55 @@ return function (Container $context): void { $route = $utopia->match($request); $path = ! empty($route) ? $route->getPath() : $request->getURI(); $orgHeader = $request->getHeader('x-appwrite-organization', ''); - if (str_starts_with($path, '/v1/projects/:projectId')) { + + // Backwards compatibility: /v1/organitation/projects acting as /v1/projects + if (\str_starts_with($path, '/v1/organization/projects')) { + // Backwards compatibility: Take from payload param + $teamId = $request->getParam('organizationId', $request->getParam('teamId', '')); + + // Backawrds compatibility: Get from URL param project ID + if (empty($teamId)) { + $projectId = \explode('/', $request->getURI())[4] ?? ''; + if (!empty($projectId)) { + $urlProject = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + $teamId = $urlProject->getAttribute('teamId', ''); + } + } + + // Backwards compatibility: Get from queries param + if (empty($teamId)) { + $queries = $request->getParam('queries', []); + $queries = \is_array($queries) ? $queries : [$queries]; + + try { + $queries = Query::parseQueries($queries); + $queries = Query::getByType($queries, [Query::TYPE_EQUAL]); + + foreach ($queries as $query) { + if ($query->getAttribute() === 'teamId') { + $teamId = $query->getValues()[0] ?? ''; + break; + } + } + } catch (QueryException $e) { + // Ignore, do not parse from queries + } + } + + // Backwards compatibility: We cannot have organization project API call without organitation, take first one + if (empty($teamId)) { + if (!$user->isEmpty()) { + $teamId = ($user->getAttribute('memberships', [])[0] ?? new Document())->getAttribute('teamId', ''); + } + } + + if (!empty($teamId)) { + $team = $authorization->skip(function () use ($dbForPlatform, $teamId) { + return $dbForPlatform->getDocument('teams', $teamId); + }); + return $team; + } + } elseif (str_starts_with($path, '/v1/projects/:projectId')) { $uri = $request->getURI(); $pid = explode('/', $uri)[3]; $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); @@ -1133,7 +1184,7 @@ return function (Container $context): void { }); return $team; - }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); + }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization', 'user']); $context->set('previewHostname', function (Request $request, ?Key $apiKey) { $allowed = false; From 2a82604a2b8a20a6f6143ebc40acbeb641acdb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 17:36:03 +0200 Subject: [PATCH 30/97] Better backwards compatibility for project creation --- .../Platform/Modules/Organization/Http/Projects/Create.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index d6f12c4f41..28f74302e2 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -20,6 +20,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\UID; use Utopia\DSN\DSN; use Utopia\Platform\Scope\HTTP; use Utopia\Pools\Group; @@ -66,6 +67,7 @@ class Create extends Action ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->param('teamId', '', new UID(), 'Team unique ID.', deprecated: true) // Backwards compatibility ->inject('response') ->inject('dbForPlatform') ->inject('cache') @@ -75,7 +77,8 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $projectId, string $name, string $region, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) + // teamId intentionally unused; used by resource for backwards compatibility + public function action(string $projectId, string $name, string $region, string $teamId, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) { $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); From 18380a679dc72405b62974bee3c7b35bda08dc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 17:46:13 +0200 Subject: [PATCH 31/97] Fix backwards compatibility --- app/config/roles.php | 2 -- app/config/scopes/organization.php | 14 ++++++++++---- .../Modules/Organization/Http/Projects/Create.php | 2 +- .../Modules/Organization/Http/Projects/Get.php | 2 +- .../Modules/Organization/Http/Projects/Update.php | 2 +- .../Modules/Organization/Http/Projects/XList.php | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index fd3b9d887d..cb4b178a29 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -19,7 +19,6 @@ $member = [ 'files.read', 'files.write', 'projects.read', - 'organization.projects.read', 'locale.read', 'avatars.read', 'executions.read', @@ -65,7 +64,6 @@ $admins = [ 'templates.read', 'templates.write', 'projects.write', - 'organization.projects.write', 'keys.read', 'keys.write', 'devKeys.read', diff --git a/app/config/scopes/organization.php b/app/config/scopes/organization.php index f531975353..0486d02c8d 100644 --- a/app/config/scopes/organization.php +++ b/app/config/scopes/organization.php @@ -3,18 +3,24 @@ // List of scopes for organization (teams) API keys return [ - "organization.projects.read" => [ - "description" => 'Access to read organization\'s projects', + "projects.read" => [ + "description" => 'Access to read organization projects.', + 'category' => 'Projects' ], - "organization.projects.write" => [ + "projects.write" => [ "description" => - "Access to create, update, and delete projects in organization", + "Access to create, update, and delete organization projects.", + 'category' => 'Projects' ], "devKeys.read" => [ "description" => 'Access to read project\'s development keys', + 'deprecated' => true, + 'category' => 'Other' ], "devKeys.write" => [ "description" => "Access to create, update, and delete project\'s development keys", + 'deprecated' => true, + 'category' => 'Other' ], ]; diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 28f74302e2..c84fa80381 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -47,7 +47,7 @@ class Create extends Action ->groups(['api', 'organization']) ->label('audits.event', 'projects.create') ->label('audits.resource', 'project/{response.$id}') - ->label('scope', 'organization.projects.write') + ->label('scope', 'projects.write') ->label('sdk', new Method( namespace: 'organization', group: 'projects', diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php index a85eb8fb15..37f2dd417a 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php @@ -29,7 +29,7 @@ class Get extends Action ->setHttpPath('/v1/organization/projects/:projectId') ->desc('Get organization project') ->groups(['api', 'organization']) - ->label('scope', 'organization.projects.read') + ->label('scope', 'projects.read') ->label('sdk', new Method( namespace: 'organization', group: 'projects', diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index 9415374044..9188060639 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->httpAlias('/v1/projects/:projectId') ->desc('Update organization project') ->groups(['api', 'organization']) - ->label('scope', 'organization.projects.write') + ->label('scope', 'projects.write') ->label('audits.event', 'projects.update') ->label('audits.resource', 'project/{response.$id}') ->label('sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index 6f04222a69..5b223904d6 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -47,7 +47,7 @@ class XList extends Action ->httpAlias('/v1/projects') ->desc('List organization projects') ->groups(['api', 'organization']) - ->label('scope', 'organization.projects.read') + ->label('scope', 'projects.read') ->label('sdk', new Method( namespace: 'organization', group: 'projects', From f37e091b770be141e1728dafa60aee45f3f276e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 17:50:36 +0200 Subject: [PATCH 32/97] Fix specs generation --- app/init/models.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init/models.php b/app/init/models.php index 521a3b77cd..b63051514c 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -263,7 +263,7 @@ Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIS Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME)); Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT)); Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION)); -Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false)); +Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, true)); Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true)); Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true)); Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false)); From bb25a36cf555a90da5caac4f56567e5fac8cd169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 17:53:03 +0200 Subject: [PATCH 33/97] Fix project creation 401 expectation tests --- app/init/resources/request.php | 7 ------- .../Platform/Modules/Organization/Http/Projects/Create.php | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 68a8af3605..6db371b8c3 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1138,13 +1138,6 @@ return function (Container $context): void { } } - // Backwards compatibility: We cannot have organization project API call without organitation, take first one - if (empty($teamId)) { - if (!$user->isEmpty()) { - $teamId = ($user->getAttribute('memberships', [])[0] ?? new Document())->getAttribute('teamId', ''); - } - } - if (!empty($teamId)) { $team = $authorization->skip(function () use ($dbForPlatform, $teamId) { return $dbForPlatform->getDocument('teams', $teamId); diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index c84fa80381..88a17cdd7a 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -67,7 +67,7 @@ class Create extends Action ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) - ->param('teamId', '', new UID(), 'Team unique ID.', deprecated: true) // Backwards compatibility + ->param('teamId', '', new UID(), 'Team unique ID.', deprecated: true, optional: true) // Backwards compatibility ->inject('response') ->inject('dbForPlatform') ->inject('cache') From 176d59e7c9ec7bc1c01e95d4936613efd1e27cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 18:02:20 +0200 Subject: [PATCH 34/97] Fix broken test after breaking change List ALL projects no longer exists - we now have list project per organization --- .../Services/Projects/ProjectsConsoleClientTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index aa5e6911f1..632636370d 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -354,6 +354,16 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); + $this->assertEquals(404, $response['headers']['status-code']); + + + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $data['teamId'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertGreaterThan(0, count($response['body']['projects'])); @@ -564,6 +574,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -7041,6 +7052,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip'])->toString(), @@ -7273,6 +7285,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, + 'x-appwrite-organization' => $teamId, ]); $this->assertEquals(200, $response['headers']['status-code']); From f40c7d415cb63d172ab23bf34d181a5a07b8b943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 18:26:47 +0200 Subject: [PATCH 35/97] Prioritize org id header --- app/init/resources/request.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 6db371b8c3..e1409d6412 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1104,6 +1104,11 @@ return function (Container $context): void { $path = ! empty($route) ? $route->getPath() : $request->getURI(); $orgHeader = $request->getHeader('x-appwrite-organization', ''); + // Prioritx #1: If org ID header present, respect it + if (!empty($orgHeader)) { + return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); + } + // Backwards compatibility: /v1/organitation/projects acting as /v1/projects if (\str_starts_with($path, '/v1/organization/projects')) { // Backwards compatibility: Take from payload param @@ -1159,8 +1164,6 @@ return function (Container $context): void { $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); return $team; - } elseif (! empty($orgHeader)) { - return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); } } From 3596662fec7121401d4043f2fa09b72567a14a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 18:42:30 +0200 Subject: [PATCH 36/97] Update more tests --- .../Services/Projects/ProjectsConsoleClientTest.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 632636370d..74d7dfcae2 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -375,6 +375,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $data['teamId'], ], $this->getHeaders(), [ 'search' => $id ])); @@ -387,6 +388,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $data['teamId'], ], $this->getHeaders(), [ 'search' => 'Project Test' ])); @@ -604,7 +606,8 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-response-format' => '1.9.4' + 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $data['teamId'], ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(), @@ -7066,6 +7069,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7078,6 +7082,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7091,6 +7096,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip', 'imagine'])->toString(), @@ -7105,6 +7111,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Test project - Labels 2', @@ -7134,6 +7141,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7149,6 +7157,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7163,6 +7172,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7178,6 +7188,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip', 'imagine'])->toString(), From d7e29a4daeab79c2761df2953e3665e1e8bbbb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 19:03:14 +0200 Subject: [PATCH 37/97] fix more tests --- .../Projects/ProjectsConsoleClientTest.php | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 74d7dfcae2..90546e1569 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -435,6 +435,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('teamId', [$team['body']['$id']])->toString(), @@ -450,6 +451,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::limit(1)->toString(), @@ -464,6 +466,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::offset(1)->toString(), @@ -477,6 +480,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('name', ['Project Test 2'])->toString(), @@ -492,6 +496,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::orderDesc()->toString(), @@ -506,6 +511,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -516,6 +522,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(), @@ -532,6 +539,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), @@ -607,7 +615,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $data['teamId'], + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(), @@ -640,6 +648,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId'])->toString(), @@ -672,6 +681,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -703,6 +713,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'platforms'])->toString(), @@ -733,6 +744,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(), @@ -763,6 +775,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['*'])->toString(), @@ -794,6 +807,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', + 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'invalidAttribute'])->toString(), @@ -1816,7 +1830,13 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals('en-us', $response['body']['locale']); /** Update Email template, fail due to SMTP disabled */ - $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([ + $projectWithoutSmtp = $this->setupProject([ + 'projectId' => ID::unique(), + 'name' => 'Project Without SMTP', + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectWithoutSmtp . '/templates/email/verification/en-us', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.1', From bcd2bfe5b272e442159b69499421edce992dd223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 19:34:49 +0200 Subject: [PATCH 38/97] Fix security --- app/controllers/shared/api.php | 4 +-- app/init/resources/request.php | 58 +++++++++++++++------------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 6f808296b0..573f6d5f06 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -315,7 +315,7 @@ Http::init() } $projectId = $project->getId(); - if ($projectId === 'console' && str_starts_with($route->getPath(), '/v1/projects/:projectId')) { + if ($projectId === 'console' && (str_starts_with($route->getPath(), '/v1/projects/:projectId') || str_starts_with($route->getPath(), '/v1/organization/projects'))) { $uri = $request->getURI(); $projectId = explode('/', $uri)[3]; } @@ -342,7 +342,7 @@ Http::init() * For console projects resource, we use platform DB. * Enabling authorization restricts admin user to the projects they have access to. */ - if ($project->getId() === 'console' && ($route->getPath() === '/v1/projects' || $route->getPath() === '/v1/projects/:projectId')) { + if ($project->getId() === 'console' && ($route->getPath() === '/v1/projects' || $route->getPath() === '/v1/projects/:projectId' || $route->getPath() === '/v1/organization/projects')) { $authorization->setDefaultStatus(true); } else { // Otherwise, disable authorization checks. diff --git a/app/init/resources/request.php b/app/init/resources/request.php index e1409d6412..2854cb2401 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1096,9 +1096,9 @@ return function (Container $context): void { }, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']); $context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization, Document $user) { - $teamInternalId = ''; + $teamId = ''; if ($project->getId() !== 'console') { - $teamInternalId = $project->getAttribute('teamInternalId', ''); + $teamId = $project->getAttribute('teamId', ''); } else { $route = $utopia->match($request); $path = ! empty($route) ? $route->getPath() : $request->getURI(); @@ -1106,12 +1106,12 @@ return function (Container $context): void { // Prioritx #1: If org ID header present, respect it if (!empty($orgHeader)) { - return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); - } + $teamId = $orgHeader; + } elseif (\str_starts_with($path, '/v1/organization/projects')) { + // Backwards compatibility: /v1/organitation/projects acting as /v1/projects - // Backwards compatibility: /v1/organitation/projects acting as /v1/projects - if (\str_starts_with($path, '/v1/organization/projects')) { // Backwards compatibility: Take from payload param + // organizationId not required, but easy to use by mistake in future unknowingly $teamId = $request->getParam('organizationId', $request->getParam('teamId', '')); // Backawrds compatibility: Get from URL param project ID @@ -1142,44 +1142,36 @@ return function (Container $context): void { // Ignore, do not parse from queries } } - - if (!empty($teamId)) { - $team = $authorization->skip(function () use ($dbForPlatform, $teamId) { - return $dbForPlatform->getDocument('teams', $teamId); - }); - return $team; - } } elseif (str_starts_with($path, '/v1/projects/:projectId')) { $uri = $request->getURI(); - $pid = explode('/', $uri)[3]; - $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); - $teamInternalId = $p->getAttribute('teamInternalId', ''); + $projectId = explode('/', $uri)[3]; + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + $teamId = $project->getAttribute('teamId', ''); } elseif ($path === '/v1/projects') { $teamId = $request->getParam('teamId', ''); + } + } - if (empty($teamId)) { - return new Document([]); - } + // No team scenario + if (empty($teamId)) { + return new Document([]); + } - $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); + $team = $authorization->skip(function () use ($dbForPlatform, $teamId) { + return $dbForPlatform->getDocument('teams', $teamId); + }); + // Unauthorized team scenario + // Ensure $user has membership in team + $memberships = $user->getAttribute('memberships', []); + foreach ($memberships as $membership) { + if ($membership->getAttribute('teamId', '') === $teamId) { return $team; } } - // if teamInternalId is empty, return an empty document - - if (empty($teamInternalId)) { - return new Document([]); - } - - $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { - return $dbForPlatform->findOne('teams', [ - Query::equal('$sequence', [$teamInternalId]), - ]); - }); - - return $team; + // Unauthorized, do not allow the team + return new Document([]); }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization', 'user']); $context->set('previewHostname', function (Request $request, ?Key $apiKey) { From 3e35b888a7b6b01169765b1c7687895014d8750a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 19:47:59 +0200 Subject: [PATCH 39/97] Fix bug --- app/controllers/shared/api.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 573f6d5f06..d900492f10 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -317,7 +317,8 @@ Http::init() $projectId = $project->getId(); if ($projectId === 'console' && (str_starts_with($route->getPath(), '/v1/projects/:projectId') || str_starts_with($route->getPath(), '/v1/organization/projects'))) { $uri = $request->getURI(); - $projectId = explode('/', $uri)[3]; + $parts = explode('/', $uri); + $projectId = str_starts_with($route->getPath(), '/v1/organization/projects') ? $parts[4] : $parts[3]; } // Base scopes for admin users to allow listing teams and projects. From 5b109f2f8ddff2fbcda1aea0d05c5aafca2cd51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 19:52:14 +0200 Subject: [PATCH 40/97] Fix org key usecase --- app/init/resources/request.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 2854cb2401..17af87affb 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1170,6 +1170,11 @@ return function (Container $context): void { } } + // API key auth bypasses membership check; key validity is verified later + if (!empty($request->getHeader('x-appwrite-key', ''))) { + return $team; + } + // Unauthorized, do not allow the team return new Document([]); }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization', 'user']); From 11839c6dcc3fc2675145325f009a7d0eca710dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:26:22 +0200 Subject: [PATCH 41/97] Remove old endpoint aliases --- .../Platform/Modules/Organization/Http/Projects/Create.php | 1 - .../Platform/Modules/Organization/Http/Projects/Update.php | 1 - .../Platform/Modules/Organization/Http/Projects/XList.php | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 88a17cdd7a..6ad1139ba7 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -42,7 +42,6 @@ class Create extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/organization/projects') - ->httpAlias('/v1/projects') ->desc('Create organization project') ->groups(['api', 'organization']) ->label('audits.event', 'projects.create') diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php index 9188060639..c364a5d6df 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -28,7 +28,6 @@ class Update extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) ->setHttpPath('/v1/organization/projects/:projectId') - ->httpAlias('/v1/projects/:projectId') ->desc('Update organization project') ->groups(['api', 'organization']) ->label('scope', 'projects.write') diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php index 5b223904d6..6b45d92175 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -44,7 +44,6 @@ class XList extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/organization/projects') - ->httpAlias('/v1/projects') ->desc('List organization projects') ->groups(['api', 'organization']) ->label('scope', 'projects.read') From 79f536445e287110ec28d950e4b01c0b976ce0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:26:35 +0200 Subject: [PATCH 42/97] Revive old endpoints --- .../Modules/Projects/Http/Projects/Create.php | 245 ++++++++++++++++++ .../Modules/Projects/Http/Projects/Update.php | 94 +++++++ .../Modules/Projects/Http/Projects/XList.php | 197 ++++++++++++++ .../Modules/Projects/Services/Http.php | 6 + 4 files changed, 542 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php create mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php create mode 100644 src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php new file mode 100644 index 0000000000..d2c92fc65c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -0,0 +1,245 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/projects') + ->desc('Create project') + ->groups(['api', 'projects']) + ->label('audits.event', 'projects.create') + ->label('audits.resource', 'project/{response.$id}') + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'create', + description: '/docs/references/projects/create.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->param('teamId', '', new UID(), 'Team unique ID.') + ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->inject('request') + ->inject('response') + ->inject('dbForPlatform') + ->inject('cache') + ->inject('pools') + ->inject('hooks') + ->callback($this->action(...)); + } + + public function action(string $projectId, string $name, string $teamId, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) + { + $team = $dbForPlatform->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); + + if (!empty($allowList) && !\in_array($region, $allowList)) { + throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported'); + } + + $auth = Config::getParam('auth', []); + $auths = [ + 'limit' => 0, + 'maxSessions' => 0, + 'passwordHistory' => 0, + 'passwordDictionary' => false, + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, + 'personalDataCheck' => false, + 'disposableEmails' => false, + 'canonicalEmails' => false, + 'freeEmails' => false, + 'mockNumbers' => [], + 'sessionAlerts' => false, + 'membershipsUserName' => false, + 'membershipsUserEmail' => false, + 'membershipsMfa' => false, + 'membershipsUserId' => false, + 'membershipsUserPhone' => false, + 'invalidateSessions' => true + ]; + + foreach ($auth as $method) { + $auths[$method['key'] ?? ''] = true; + } + + $projectId = ($projectId == 'unique()') ? ID::unique() : $projectId; + + if ($projectId === 'console') { + throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); + } + + $databases = Config::getParam('pools-database', []); + + if ($region !== 'default') { + $databaseKeys = System::getEnv('_APP_DATABASE_KEYS', ''); + $keys = explode(',', $databaseKeys); + $databases = array_filter($keys, function ($value) use ($region) { + return str_contains($value, $region); + }); + } + + $databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE'); + $index = \array_search($databaseOverride, $databases); + if ($index !== false) { + $dsn = $databases[$index]; + } else { + $dsn = $databases[array_rand($databases)]; + } + + // TODO: Temporary until all projects are using shared tables. + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn, $sharedTables)) { + $schema = 'appwrite'; + $database = 'appwrite'; + $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''); + $dsn = $schema . '://' . $dsn . '?database=' . $database; + + if (!empty($namespace)) { + $dsn .= '&namespace=' . $namespace; + } + } + + try { + $project = $dbForPlatform->createDocument('projects', new Document([ + '$id' => $projectId, + '$permissions' => $this->getPermissions($teamId, $projectId), + 'name' => $name, + 'teamInternalId' => $team->getSequence(), + 'teamId' => $team->getId(), + 'region' => $region, + 'version' => APP_VERSION_STABLE, + 'services' => new \stdClass(), + 'platforms' => null, + 'oAuthProviders' => [], + 'webhooks' => null, + 'keys' => null, + 'auths' => $auths, + 'accessedAt' => DateTime::now(), + 'search' => implode(' ', [$projectId, $name]), + 'database' => $dsn, + 'labels' => [], + 'status' => PROJECT_STATUS_ACTIVE, + ])); + } catch (Duplicate) { + throw new Exception(Exception::PROJECT_ALREADY_EXISTS); + } + + try { + $dsn = new DSN($dsn); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $dsn); + } + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $projectTables = !\in_array($dsn->getHost(), $sharedTables); + + if ($projectTables) { + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $dbForProject = new Database($adapter, $cache); + $dbForProject + ->setDatabase(APP_DATABASE) + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + + $create = true; + + try { + $dbForProject->create(); + } catch (Duplicate) { + $create = false; + } + + $adapter = new AdapterDatabase($dbForProject); + $audit = new Audit($adapter); + $audit->setup(); + + if ($create) { + /** @var array $collections */ + $collections = Config::getParam('collections', [])['projects'] ?? []; + + foreach ($collections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + + $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); + $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); + + try { + $dbForProject->createCollection($key, $attributes, $indexes); + } catch (Duplicate) { + // Collection already exists + } + } + } + } + + // Hook allowing instant project mirroring during migration + // Outside of migration, hook is not registered and has no effect + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php new file mode 100644 index 0000000000..29c26b33ea --- /dev/null +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php @@ -0,0 +1,94 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/projects/:projectId') + ->desc('Update project') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('audits.event', 'projects.update') + ->label('audits.resource', 'project/{request.projectId}') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'update', + description: '/docs/references/projects/update.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) + ->param('logo', '', new Text(1024), 'Project logo.', true) + ->param('url', '', new URL(), 'Project URL.', true) + ->param('legalName', '', new Text(256), 'Project legal name. Max length: 256 chars.', true) + ->param('legalCountry', '', new Text(256), 'Project legal country. Max length: 256 chars.', true) + ->param('legalState', '', new Text(256), 'Project legal state. Max length: 256 chars.', true) + ->param('legalCity', '', new Text(256), 'Project legal city. Max length: 256 chars.', true) + ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) + ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action(string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) + { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project + ->setAttribute('name', $name) + ->setAttribute('description', $description) + ->setAttribute('logo', $logo) + ->setAttribute('url', $url) + ->setAttribute('legalName', $legalName) + ->setAttribute('legalCountry', $legalCountry) + ->setAttribute('legalState', $legalState) + ->setAttribute('legalCity', $legalCity) + ->setAttribute('legalAddress', $legalAddress) + ->setAttribute('legalTaxId', $legalTaxId) + ->setAttribute('search', implode(' ', [$projectId, $name]))); + + $response->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php new file mode 100644 index 0000000000..0d2a951388 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -0,0 +1,197 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/projects') + ->desc('List projects') + ->groups(['api', 'projects']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'list', + description: <<param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('response') + ->inject('dbForPlatform') + ->inject('team') + ->callback($this->action(...)); + } + + public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) + { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + if (!$team->isEmpty()) { + $queries[] = Query::equal('teamInternalId', [$team->getSequence()]); + } + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $projectId = $cursor->getValue(); + $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + try { + $selectQueries = Query::groupByType($queries)['selections']; + $filterQueries = Query::groupByType($queries)['filters']; + + $projects = $this->find($dbForPlatform, $queries, $selectQueries); + $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (Order $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + + $response->addFilter(new ListSelection($selectQueries, 'projects')); + + $response->dynamic(new Document([ + 'projects' => $projects, + 'total' => $total, + ]), Response::MODEL_PROJECT_LIST); + } + + // Build mapping of columns to their subQuery filters + private static function getAttributeToSubQueryFilters(): array + { + if (self::$attributeToSubQueryFilters !== null) { + return self::$attributeToSubQueryFilters; + } + + self::$attributeToSubQueryFilters = []; + + $collections = Config::getParam('collections', []); + $projectAttributes = $collections['platform']['projects']['attributes'] ?? []; + + foreach ($projectAttributes as $attribute) { + $attributeId = $attribute['$id'] ?? null; + $filters = $attribute['filters'] ?? []; + + if ($attributeId === null || empty($filters)) { + continue; + } + + // extract only subQuery filters + $subQueryFilters = \array_filter($filters, function ($filter) { + return \str_starts_with($filter, 'subQuery'); + }); + + if (!empty($subQueryFilters)) { + self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters); + } + } + + return self::$attributeToSubQueryFilters; + } + + private function find(Database $dbForPlatform, array $queries, array $selectQueries): array + { + if (empty($selectQueries)) { + return $dbForPlatform->find('projects', $queries); + } + + $selectedAttributes = []; + foreach ($selectQueries as $query) { + foreach ($query->getValues() as $value) { + $selectedAttributes[] = $value; + } + } + + if (\in_array('*', $selectedAttributes)) { + return $dbForPlatform->find('projects', $queries); + } + + $filtersToSkipMap = []; + $selectedAttributesMap = \array_flip($selectedAttributes); + $attributeToSubQueryFilters = self::getAttributeToSubQueryFilters(); + + foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) { + if (!isset($selectedAttributesMap[$attributeName])) { + foreach ($subQueryFilters as $filter) { + $filtersToSkipMap[$filter] = true; + } + } + } + + $filtersToSkip = \array_keys($filtersToSkipMap); + + return empty($filtersToSkip) + ? $dbForPlatform->find('projects', $queries) + : $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip); + } +} diff --git a/src/Appwrite/Platform/Modules/Projects/Services/Http.php b/src/Appwrite/Platform/Modules/Projects/Services/Http.php index b1662cef31..8275e664d5 100644 --- a/src/Appwrite/Platform/Modules/Projects/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Projects/Services/Http.php @@ -7,7 +7,10 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey; use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys; +use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject; use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam; +use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject; +use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects; use Appwrite\Platform\Modules\Projects\Http\Schedules\Create as CreateSchedule; use Appwrite\Platform\Modules\Projects\Http\Schedules\Get as GetSchedule; use Appwrite\Platform\Modules\Projects\Http\Schedules\XList as ListSchedules; @@ -24,6 +27,9 @@ class Http extends Service $this->addAction(ListDevKeys::getName(), new ListDevKeys()); $this->addAction(DeleteDevKey::getName(), new DeleteDevKey()); + $this->addAction(CreateProject::getName(), new CreateProject()); + $this->addAction(UpdateProject::getName(), new UpdateProject()); + $this->addAction(ListProjects::getName(), new ListProjects()); $this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam()); $this->addAction(CreateSchedule::getName(), new CreateSchedule()); From cd54e9178468edeb9a94ae21fcee8f272c2c0180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:27:19 +0200 Subject: [PATCH 43/97] Deprecate old projects endpoints --- .../Modules/Projects/Http/Projects/Create.php | 13 ------------- .../Modules/Projects/Http/Projects/Update.php | 13 ------------- .../Modules/Projects/Http/Projects/XList.php | 16 ---------------- 3 files changed, 42 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index d2c92fc65c..b5873228cc 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -54,19 +54,6 @@ class Create extends Action ->label('audits.event', 'projects.create') ->label('audits.resource', 'project/{response.$id}') ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'create', - description: '/docs/references/projects/create.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_PROJECT, - ) - ] - )) ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('teamId', '', new UID(), 'Team unique ID.') diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php index 29c26b33ea..c1694a029c 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php @@ -39,19 +39,6 @@ class Update extends Action ->label('scope', 'projects.write') ->label('audits.event', 'projects.update') ->label('audits.resource', 'project/{request.projectId}') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'update', - description: '/docs/references/projects/update.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 0d2a951388..98318fe2a8 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -48,22 +48,6 @@ class XList extends Action ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'list', - description: <<param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) From 2d9470494800e10f21e71a8346ef840cba7258f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:30:06 +0200 Subject: [PATCH 44/97] Formatting fix --- .../Platform/Modules/Projects/Http/Projects/Create.php | 3 --- .../Platform/Modules/Projects/Http/Projects/Update.php | 3 --- .../Platform/Modules/Projects/Http/Projects/XList.php | 4 ---- 3 files changed, 10 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index b5873228cc..18250cb140 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -4,9 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php index c1694a029c..f6df843d07 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php @@ -3,9 +3,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 98318fe2a8..b967c29451 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -4,10 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\ContentType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\ListSelection; From 34078ad9afe011f801f48f82aee2c13ddf117577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:37:04 +0200 Subject: [PATCH 45/97] Revert unwanted changes --- app/config/scopes/organization.php | 4 ++-- .../Platform/Modules/Organization/Http/Projects/Create.php | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/config/scopes/organization.php b/app/config/scopes/organization.php index 4e10331b7f..d74452f259 100644 --- a/app/config/scopes/organization.php +++ b/app/config/scopes/organization.php @@ -13,13 +13,13 @@ return [ "category" => "Projects", ], "devKeys.read" => [ - "description" => 'Access to read organization project\'s development keys', + "description" => 'Access to read project\'s development keys', "category" => "Other", "deprecated" => true, ], "devKeys.write" => [ "description" => - "Access to create, update, and delete organization project\'s development keys", + "Access to create, update, and delete project\'s development keys", "category" => "Other", "deprecated" => true, ], diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 6ad1139ba7..c4c5278d1f 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -66,7 +66,6 @@ class Create extends Action ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) - ->param('teamId', '', new UID(), 'Team unique ID.', deprecated: true, optional: true) // Backwards compatibility ->inject('response') ->inject('dbForPlatform') ->inject('cache') @@ -76,8 +75,7 @@ class Create extends Action ->callback($this->action(...)); } - // teamId intentionally unused; used by resource for backwards compatibility - public function action(string $projectId, string $name, string $region, string $teamId, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) + public function action(string $projectId, string $name, string $region, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) { $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); From d422b7abdcb887976b2dd7640d1fdce5c638b718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:44:47 +0200 Subject: [PATCH 46/97] Final reverts --- app/controllers/shared/api.php | 7 ++- app/init/resources/request.php | 96 +++++++++------------------------- 2 files changed, 29 insertions(+), 74 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5bcb51355b..6e5167660a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -313,10 +313,9 @@ Http::init() } $projectId = $project->getId(); - if ($projectId === 'console' && (str_starts_with($route->getPath(), '/v1/projects/:projectId') || str_starts_with($route->getPath(), '/v1/organization/projects'))) { + if ($projectId === 'console' && str_starts_with($route->getPath(), '/v1/projects/:projectId')) { $uri = $request->getURI(); - $parts = explode('/', $uri); - $projectId = str_starts_with($route->getPath(), '/v1/organization/projects') ? $parts[4] : $parts[3]; + $projectId = explode('/', $uri)[3]; } // Base scopes for admin users to allow listing teams and projects. @@ -341,7 +340,7 @@ Http::init() * For console projects resource, we use platform DB. * Enabling authorization restricts admin user to the projects they have access to. */ - if ($project->getId() === 'console' && ($route->getPath() === '/v1/projects' || $route->getPath() === '/v1/projects/:projectId' || $route->getPath() === '/v1/organization/projects')) { + if ($project->getId() === 'console' && ($route->getPath() === '/v1/projects' || $route->getPath() === '/v1/projects/:projectId')) { $authorization->setDefaultStatus(true); } else { // Otherwise, disable authorization checks. diff --git a/app/init/resources/request.php b/app/init/resources/request.php index e6382be032..85d8db3698 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -1,6 +1,5 @@ set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization, Document $user) { - $teamId = ''; + $context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) { + $teamInternalId = ''; if ($project->getId() !== 'console') { - $teamId = $project->getAttribute('teamId', ''); + $teamInternalId = $project->getAttribute('teamInternalId', ''); } else { $route = $utopia->match($request); $path = ! empty($route) ? $route->getPath() : $request->getURI(); $orgHeader = $request->getHeader('x-appwrite-organization', ''); - - // Prioritx #1: If org ID header present, respect it - if (!empty($orgHeader)) { - $teamId = $orgHeader; - } elseif (\str_starts_with($path, '/v1/organization/projects')) { - // Backwards compatibility: /v1/organitation/projects acting as /v1/projects - - // Backwards compatibility: Take from payload param - // organizationId not required, but easy to use by mistake in future unknowingly - $teamId = $request->getParam('organizationId', $request->getParam('teamId', '')); - - // Backawrds compatibility: Get from URL param project ID - if (empty($teamId)) { - $projectId = \explode('/', $request->getURI())[4] ?? ''; - if (!empty($projectId)) { - $urlProject = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - $teamId = $urlProject->getAttribute('teamId', ''); - } - } - - // Backwards compatibility: Get from queries param - if (empty($teamId)) { - $queries = $request->getParam('queries', []); - $queries = \is_array($queries) ? $queries : [$queries]; - - try { - $queries = Query::parseQueries($queries); - $queries = Query::getByType($queries, [Query::TYPE_EQUAL]); - - foreach ($queries as $query) { - if ($query->getAttribute() === 'teamId') { - $teamId = $query->getValues()[0] ?? ''; - break; - } - } - } catch (QueryException $e) { - // Ignore, do not parse from queries - } - } - } elseif (str_starts_with($path, '/v1/projects/:projectId')) { + if (str_starts_with($path, '/v1/projects/:projectId')) { $uri = $request->getURI(); - $projectId = explode('/', $uri)[3]; - $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - $teamId = $project->getAttribute('teamId', ''); + $pid = explode('/', $uri)[3]; + $p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid)); + $teamInternalId = $p->getAttribute('teamInternalId', ''); } elseif ($path === '/v1/projects') { $teamId = $request->getParam('teamId', ''); + + if (empty($teamId)) { + return new Document([]); + } + + $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); + + return $team; + } elseif (! empty($orgHeader)) { + return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); } } - // No team scenario - if (empty($teamId)) { + // if teamInternalId is empty, return an empty document + + if (empty($teamInternalId)) { return new Document([]); } - $team = $authorization->skip(function () use ($dbForPlatform, $teamId) { - return $dbForPlatform->getDocument('teams', $teamId); + $team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) { + return $dbForPlatform->findOne('teams', [ + Query::equal('$sequence', [$teamInternalId]), + ]); }); - // Unauthorized team scenario - // Ensure $user has membership in team - $memberships = $user->getAttribute('memberships', []); - foreach ($memberships as $membership) { - if ($membership->getAttribute('teamId', '') === $teamId) { - return $team; - } - } - - // API key auth bypasses membership check; key validity is verified later - if (!empty($request->getHeader('x-appwrite-key', ''))) { - return $team; - } - - // Unauthorized, do not allow the team - return new Document([]); - }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization', 'user']); + return $team; + }, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']); $context->set('previewHostname', function (Request $request, ?Key $apiKey) { $allowed = false; From 5a0bb57db25c7aff8cb8f209022ab025815584f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:45:39 +0200 Subject: [PATCH 47/97] linter fix --- .../Platform/Modules/Organization/Http/Projects/Create.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index c4c5278d1f..227c64ce8c 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -20,7 +20,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; -use Utopia\Database\Validator\UID; use Utopia\DSN\DSN; use Utopia\Platform\Scope\HTTP; use Utopia\Pools\Group; From a0c27afec8a08711a7be8b393c64d57fc5ab7fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 11:47:52 +0200 Subject: [PATCH 48/97] Revert test changes --- .../Projects/ProjectsConsoleClientTest.php | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 90546e1569..7b44b0485d 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -354,16 +354,6 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); - $this->assertEquals(404, $response['headers']['status-code']); - - - $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $data['teamId'], - ], $this->getHeaders())); - $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertGreaterThan(0, count($response['body']['projects'])); @@ -375,7 +365,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $data['teamId'], ], $this->getHeaders(), [ 'search' => $id ])); @@ -388,7 +377,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $data['teamId'], ], $this->getHeaders(), [ 'search' => 'Project Test' ])); @@ -435,7 +423,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('teamId', [$team['body']['$id']])->toString(), @@ -451,7 +438,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::limit(1)->toString(), @@ -466,7 +452,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::offset(1)->toString(), @@ -480,7 +465,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::equal('name', ['Project Test 2'])->toString(), @@ -496,7 +480,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::orderDesc()->toString(), @@ -511,7 +494,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -522,7 +504,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(), @@ -539,7 +520,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $team['body']['$id'], ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), @@ -584,7 +564,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -614,8 +593,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, + 'x-appwrite-response-format' => '1.9.4' ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(), @@ -648,7 +626,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId'])->toString(), @@ -681,7 +658,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -713,7 +689,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'platforms'])->toString(), @@ -744,7 +719,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(), @@ -775,7 +749,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['*'])->toString(), @@ -807,7 +780,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'invalidAttribute'])->toString(), @@ -7075,7 +7047,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip'])->toString(), @@ -7089,7 +7060,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7102,7 +7072,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7116,7 +7085,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip', 'imagine'])->toString(), @@ -7131,7 +7099,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Test project - Labels 2', @@ -7161,7 +7128,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7177,7 +7143,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7192,7 +7157,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7208,7 +7172,6 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', - 'x-appwrite-organization' => $teamId, ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip', 'imagine'])->toString(), @@ -7316,7 +7279,6 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, - 'x-appwrite-organization' => $teamId, ]); $this->assertEquals(200, $response['headers']['status-code']); From b627a7d6ff4e7f524fb2d450b5f11f5aa7986561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 May 2026 12:46:27 +0200 Subject: [PATCH 49/97] Org key tests --- tests/e2e/Scopes/SideServerOrganization.php | 81 +++++++++++++++++++ .../Services/Organization/ProjectsBase.php | 26 ++++++ .../ProjectsConsoleClientTest.php | 14 ++++ .../Organization/ProjectsCustomServerTest.php | 14 ++++ 4 files changed, 135 insertions(+) create mode 100644 tests/e2e/Scopes/SideServerOrganization.php create mode 100644 tests/e2e/Services/Organization/ProjectsBase.php create mode 100644 tests/e2e/Services/Organization/ProjectsConsoleClientTest.php create mode 100644 tests/e2e/Services/Organization/ProjectsCustomServerTest.php diff --git a/tests/e2e/Scopes/SideServerOrganization.php b/tests/e2e/Scopes/SideServerOrganization.php new file mode 100644 index 0000000000..45fa48451f --- /dev/null +++ b/tests/e2e/Scopes/SideServerOrganization.php @@ -0,0 +1,81 @@ +client->call(Client::METHOD_POST, '/teams', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'teamId' => $teamId, + 'name' => 'Organization Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409]); + $teamId = $team['body']['$id'] ?? $teamId; + + $key = $this->client->call(Client::METHOD_POST, '/v1/organization/keys', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + 'x-appwrite-organization' => $teamId, + ], [ + 'keyId' => ID::unique(), + 'name' => 'Organization Key', + 'scopes' => ['projects.read', 'projects.write'], + ]); + + $this->assertEquals(201, $key['headers']['status-code']); + $this->assertNotEmpty($key['body']['secret']); + + self::$organization = [ + '$id' => $teamId, + 'apiKey' => $key['body']['secret'], + ]; + + return self::$organization; + } + + public function getHeaders(bool $devKey = false): array + { + $organization = $this->getOrganization(); + + return [ + 'x-appwrite-key' => $organization['apiKey'], + 'x-appwrite-organization' => $organization['$id'], + ]; + } + + /** + * @return string + */ + public function getSide() + { + return 'server'; + } +} diff --git a/tests/e2e/Services/Organization/ProjectsBase.php b/tests/e2e/Services/Organization/ProjectsBase.php new file mode 100644 index 0000000000..eff7c827a3 --- /dev/null +++ b/tests/e2e/Services/Organization/ProjectsBase.php @@ -0,0 +1,26 @@ +client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Organization Project Test', $response['body']['name']); + } +} diff --git a/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php b/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php new file mode 100644 index 0000000000..5d016eff01 --- /dev/null +++ b/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php @@ -0,0 +1,14 @@ + Date: Tue, 19 May 2026 11:21:04 +0530 Subject: [PATCH 50/97] feat: remove CommitSkipPatterns - already available in 1.9.x Keep only branch and path skip trigger logic. Commit message skip is handled by the existing Contains(VCS_DEPLOYMENT_SKIP_PATTERNS) check. Co-Authored-By: Claude Sonnet 4.6 --- app/config/collections/projects.php | 22 -- .../Vcs/Validator/CommitSkipPatterns.php | 85 ------- .../Vcs/Validator/CommitSkipPatternsTest.php | 215 ------------------ 3 files changed, 322 deletions(-) delete mode 100644 src/Appwrite/Vcs/Validator/CommitSkipPatterns.php delete mode 100644 tests/unit/Vcs/Validator/CommitSkipPatternsTest.php diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 30c82fbef0..120c9704ce 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -863,17 +863,6 @@ return [ 'array' => true, 'filters' => [], ], - [ - '$id' => ID::custom('providerCommitSkipPatterns'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 128, - 'signed' => true, - 'required' => false, - 'default' => [], - 'array' => true, - 'filters' => [], - ], ], 'indexes' => [ [ @@ -1375,17 +1364,6 @@ return [ 'array' => true, 'filters' => [], ], - [ - '$id' => ID::custom('providerCommitSkipPatterns'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 128, - 'signed' => true, - 'required' => false, - 'default' => [], - 'array' => true, - 'filters' => [], - ], ], 'indexes' => [ [ diff --git a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php b/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php deleted file mode 100644 index 9dbe45ba83..0000000000 --- a/src/Appwrite/Vcs/Validator/CommitSkipPatterns.php +++ /dev/null @@ -1,85 +0,0 @@ -patterns as $pattern) { - if (!is_string($pattern)) { - continue; - } - - $pattern = trim($pattern); - if ($pattern === '') { - continue; - } - - // Split on whitespace; each token is regex-quoted. Tokens are rejoined - // with \s+ (required space) so that "skipappwrite" does NOT match the - // pattern "skip appwrite". The only exception: when the preceding token - // ends with ":" (git trailer style), \s* is used so that - // "skip-checks:true" still matches the pattern "skip-checks: true". - $tokens = preg_split('/\s+/', $pattern); - $regexParts = []; - $count = count($tokens); - for ($i = 0; $i < $count; $i++) { - $regexParts[] = preg_quote($tokens[$i], '~'); - if ($i < $count - 1) { - $regexParts[] = str_ends_with($tokens[$i], ':') ? '\s*' : '\s+'; - } - } - $regexBody = implode('', $regexParts); - - // (?assertTrue($validator->isValid('fix: update readme')); - $this->assertTrue($validator->isValid('[skip deploy] docs only')); - $this->assertTrue($validator->isValid('')); - } - - // ------------------------------------------------------------------------- - // Single pattern — directive match - // ------------------------------------------------------------------------- - - public function testSinglePatternMatchSkips(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $this->assertFalse($validator->isValid('[skip deploy] docs only')); - $this->assertFalse($validator->isValid('chore: update deps [skip deploy]')); - $this->assertFalse($validator->isValid('prefix [skip deploy] suffix')); - } - - public function testSinglePatternNoMatchProceeds(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $this->assertTrue($validator->isValid('fix: real bug fix')); - $this->assertTrue($validator->isValid('feat: add new feature')); - $this->assertTrue($validator->isValid('skip deploy without brackets')); - $this->assertTrue($validator->isValid('prefix[skip deploy]suffix')); - } - - // ------------------------------------------------------------------------- - // Case insensitivity - // ------------------------------------------------------------------------- - - public function testCaseInsensitiveMatch(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $this->assertFalse($validator->isValid('[SKIP DEPLOY] uppercase')); - $this->assertFalse($validator->isValid('[Skip Deploy] mixed case')); - $this->assertFalse($validator->isValid('[skip DEPLOY] partial upper')); - } - - public function testPatternItselfCaseInsensitive(): void - { - $validator = new CommitSkipPatterns(['[SKIP DEPLOY]']); - $this->assertFalse($validator->isValid('[skip deploy] lowercase message')); - $this->assertFalse($validator->isValid('[Skip Deploy] mixed message')); - } - - // ------------------------------------------------------------------------- - // Array of patterns — any match skips (OR semantics) - // ------------------------------------------------------------------------- - - public function testMultiplePatternsFirstMatches(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); - $this->assertFalse($validator->isValid('[skip deploy] docs only')); - } - - public function testMultiplePatternsSecondMatches(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); - $this->assertFalse($validator->isValid('chore: update readme [skip ci]')); - } - - public function testMultiplePatternsThirdMatches(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); - $this->assertFalse($validator->isValid('[no deploy] just docs')); - } - - public function testMultiplePatternsNoneMatchProceeds(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]', '[skip ci]', '[no deploy]']); - $this->assertTrue($validator->isValid('feat: completely new feature')); - $this->assertTrue($validator->isValid('fix: important bug fix')); - } - - // ------------------------------------------------------------------------- - // Common real-world skip conventions - // ------------------------------------------------------------------------- - - public function testCommonSkipCiPattern(): void - { - $validator = new CommitSkipPatterns(['[skip ci]']); - $this->assertFalse($validator->isValid('[skip ci] update changelog')); - $this->assertFalse($validator->isValid('[SKIP CI]')); - $this->assertTrue($validator->isValid('feat: something real')); - } - - public function testNoDeployPattern(): void - { - $validator = new CommitSkipPatterns(['[no deploy]']); - $this->assertFalse($validator->isValid('[no deploy] tweak docs')); - $this->assertTrue($validator->isValid('deploy this please')); - } - - // ------------------------------------------------------------------------- - // Edge cases - // ------------------------------------------------------------------------- - - public function testEmptyCommitMessageNeverSkipsWithPatterns(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $this->assertTrue($validator->isValid('')); - } - - public function testBlankPatternsInArrayAreIgnored(): void - { - $validator = new CommitSkipPatterns(['', ' ', '[skip deploy]']); - $this->assertTrue($validator->isValid('normal commit message')); - $this->assertFalse($validator->isValid('[skip deploy] docs')); - } - - public function testPatternMustBeStandaloneDirective(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $this->assertTrue($validator->isValid('skippy the kangaroo')); - $this->assertTrue($validator->isValid('prefix[skip deploy]suffix')); - } - - public function testMultilineCommitMessage(): void - { - $validator = new CommitSkipPatterns(['[skip deploy]']); - $msg = "feat: add new stuff\n\nMore detail here.\n\n[skip deploy]"; - $this->assertFalse($validator->isValid($msg)); - } - - public function testWhitespaceInsideDirectiveIsNormalized(): void - { - $validator = new CommitSkipPatterns([' [skip deploy] ']); - $this->assertFalse($validator->isValid('[skip deploy] docs only')); - $this->assertFalse($validator->isValid('[SKIP DEPLOY] docs only')); - } - - public function testTrailerDirectiveCanSkip(): void - { - $validator = new CommitSkipPatterns(['skip-checks: true']); - $msg = "feat: add new stuff\n\nMore detail here.\n\nskip-checks:true"; - $this->assertFalse($validator->isValid($msg)); - } - - // ------------------------------------------------------------------------- - // Plain-word patterns: "skip appwrite" and "appwrite skip" - // ------------------------------------------------------------------------- - - public function testSkipAppwritePatternSkips(): void - { - $validator = new CommitSkipPatterns(['skip appwrite']); - $this->assertFalse($validator->isValid('docs: update readme skip appwrite')); - $this->assertFalse($validator->isValid('skip appwrite')); - } - - public function testSkipAppwritePatternCaseInsensitive(): void - { - $validator = new CommitSkipPatterns(['skip appwrite']); - $this->assertFalse($validator->isValid('SKIP APPWRITE')); - $this->assertFalse($validator->isValid('Skip Appwrite')); - $this->assertFalse($validator->isValid('SKIP appwrite')); - } - - public function testSkipAppwritePatternNoMatchProceeds(): void - { - $validator = new CommitSkipPatterns(['skip appwrite']); - $this->assertTrue($validator->isValid('feat: real feature')); - $this->assertTrue($validator->isValid('skipappwrite')); // no space — not standalone - $this->assertTrue($validator->isValid('appwrite is great')); - } - - public function testAppwriteSkipPatternSkips(): void - { - $validator = new CommitSkipPatterns(['appwrite skip']); - $this->assertFalse($validator->isValid('appwrite skip ci')); - $this->assertFalse($validator->isValid('docs appwrite skip')); - $this->assertFalse($validator->isValid('appwrite skip')); - } - - public function testAppwriteSkipPatternCaseInsensitive(): void - { - $validator = new CommitSkipPatterns(['appwrite skip']); - $this->assertFalse($validator->isValid('APPWRITE SKIP')); - $this->assertFalse($validator->isValid('Appwrite Skip')); - $this->assertFalse($validator->isValid('appwrite SKIP')); - } - - public function testAppwriteSkipPatternNoMatchProceeds(): void - { - $validator = new CommitSkipPatterns(['appwrite skip']); - $this->assertTrue($validator->isValid('feat: deploy appwrite changes')); - $this->assertTrue($validator->isValid('appwriteskip')); // no space — not standalone - $this->assertTrue($validator->isValid('skip the appwrite stuff')); - } - - public function testBothAppwritePatternsInArray(): void - { - $validator = new CommitSkipPatterns(['skip appwrite', 'appwrite skip']); - $this->assertFalse($validator->isValid('skip appwrite')); - $this->assertFalse($validator->isValid('appwrite skip')); - $this->assertFalse($validator->isValid('SKIP APPWRITE')); - $this->assertFalse($validator->isValid('APPWRITE SKIP')); - $this->assertTrue($validator->isValid('feat: deploy appwrite changes')); - } -} From 74a4ae19eea38d4cb61686ee3e65cee79b30f6f5 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 19 May 2026 11:28:04 +0530 Subject: [PATCH 51/97] refactor: remove unnecessary comments Co-Authored-By: Claude Sonnet 4.6 --- .../Modules/VCS/Http/GitHub/Authorize/External/Update.php | 2 +- .../Platform/Modules/VCS/Http/GitHub/Events/Create.php | 2 +- src/Appwrite/Vcs/Validator/BuildTrigger.php | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) 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 fed7248ddf..86ac0899d1 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 @@ -133,7 +133,7 @@ class Update extends Action $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), - ...array_filter(array_column($prFiles, 'previous_filename')) // Filter out null values + ...array_filter(array_column($prFiles, 'previous_filename')) ]; $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); 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 fd8a48ac15..7ce9ece505 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -215,7 +215,7 @@ class Create extends Action $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), - ...array_filter(array_column($prFiles, 'previous_filename')) // Filter out null values + ...array_filter(array_column($prFiles, 'previous_filename')) ]; $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php index 22037c8907..be8b9c43d1 100644 --- a/src/Appwrite/Vcs/Validator/BuildTrigger.php +++ b/src/Appwrite/Vcs/Validator/BuildTrigger.php @@ -24,7 +24,6 @@ class BuildTrigger extends Validator $exclude = array_filter($this->patterns, fn ($p) => str_starts_with($p, '!')); if (empty($include)) { - // Only exclusions: pass everything unless excluded. foreach ($exclude as $pattern) { if ($this->matchGlob($value, substr($pattern, 1))) { return false; @@ -33,31 +32,26 @@ class BuildTrigger extends Validator return true; } - // A pattern is "specific" when it contains no wildcard characters. $isSpecific = fn ($pattern) => !str_contains($pattern, '*') && !str_contains($pattern, '?'); - // 1. Specific inclusion always wins — an explicit exact match is never blocked. foreach ($include as $pattern) { if ($isSpecific($pattern) && $this->matchGlob($value, $pattern)) { return true; } } - // 2. Any exclusion (specific or wildcard) overrides a wildcard inclusion — refines broad patterns. foreach ($exclude as $pattern) { if ($this->matchGlob($value, substr($pattern, 1))) { return false; } } - // 3. Wildcard inclusion — no exclusion blocked it. foreach ($include as $pattern) { if (!$isSpecific($pattern) && $this->matchGlob($value, $pattern)) { return true; } } - // No inclusion matched. return false; } From b2ebfa9bb9d9c83b2fd8172dc15e5a3147b69cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 19 May 2026 17:20:15 +0200 Subject: [PATCH 52/97] Revert unwanted tests --- tests/e2e/Scopes/SideServerOrganization.php | 81 ------------------- .../Organization/ProjectsCustomServerTest.php | 14 ---- 2 files changed, 95 deletions(-) delete mode 100644 tests/e2e/Scopes/SideServerOrganization.php delete mode 100644 tests/e2e/Services/Organization/ProjectsCustomServerTest.php diff --git a/tests/e2e/Scopes/SideServerOrganization.php b/tests/e2e/Scopes/SideServerOrganization.php deleted file mode 100644 index 45fa48451f..0000000000 --- a/tests/e2e/Scopes/SideServerOrganization.php +++ /dev/null @@ -1,81 +0,0 @@ -client->call(Client::METHOD_POST, '/teams', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'teamId' => $teamId, - 'name' => 'Organization Test', - ]); - if (\in_array($team['headers']['status-code'], [201, 409])) { - break; - } - \usleep(500000); - } - $this->assertContains($team['headers']['status-code'], [201, 409]); - $teamId = $team['body']['$id'] ?? $teamId; - - $key = $this->client->call(Client::METHOD_POST, '/v1/organization/keys', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - 'x-appwrite-organization' => $teamId, - ], [ - 'keyId' => ID::unique(), - 'name' => 'Organization Key', - 'scopes' => ['projects.read', 'projects.write'], - ]); - - $this->assertEquals(201, $key['headers']['status-code']); - $this->assertNotEmpty($key['body']['secret']); - - self::$organization = [ - '$id' => $teamId, - 'apiKey' => $key['body']['secret'], - ]; - - return self::$organization; - } - - public function getHeaders(bool $devKey = false): array - { - $organization = $this->getOrganization(); - - return [ - 'x-appwrite-key' => $organization['apiKey'], - 'x-appwrite-organization' => $organization['$id'], - ]; - } - - /** - * @return string - */ - public function getSide() - { - return 'server'; - } -} diff --git a/tests/e2e/Services/Organization/ProjectsCustomServerTest.php b/tests/e2e/Services/Organization/ProjectsCustomServerTest.php deleted file mode 100644 index 68caa2f299..0000000000 --- a/tests/e2e/Services/Organization/ProjectsCustomServerTest.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Wed, 20 May 2026 13:49:29 +0200 Subject: [PATCH 53/97] PR cleanup --- docs/references/projects/create-jwt.md | 1 - docs/references/projects/create-smtp-test.md | 1 - docs/references/projects/create.md | 1 - docs/references/projects/delete-email-template.md | 1 - docs/references/projects/delete.md | 1 - docs/references/projects/get-email-template.md | 1 - docs/references/projects/get.md | 1 - docs/references/projects/update-auth-status.md | 1 - docs/references/projects/update-email-template.md | 1 - docs/references/projects/update-mock-numbers.md | 1 - docs/references/projects/update-oauth2.md | 1 - docs/references/projects/update-smtp.md | 1 - docs/references/projects/update.md | 1 - 13 files changed, 13 deletions(-) delete mode 100644 docs/references/projects/create-jwt.md delete mode 100644 docs/references/projects/create-smtp-test.md delete mode 100644 docs/references/projects/create.md delete mode 100644 docs/references/projects/delete-email-template.md delete mode 100644 docs/references/projects/delete.md delete mode 100644 docs/references/projects/get-email-template.md delete mode 100644 docs/references/projects/get.md delete mode 100644 docs/references/projects/update-auth-status.md delete mode 100644 docs/references/projects/update-email-template.md delete mode 100644 docs/references/projects/update-mock-numbers.md delete mode 100644 docs/references/projects/update-oauth2.md delete mode 100644 docs/references/projects/update-smtp.md delete mode 100644 docs/references/projects/update.md diff --git a/docs/references/projects/create-jwt.md b/docs/references/projects/create-jwt.md deleted file mode 100644 index 9a6f8ebf6b..0000000000 --- a/docs/references/projects/create-jwt.md +++ /dev/null @@ -1 +0,0 @@ -Create a new JWT token. This token can be used to authenticate users with custom scopes and expiration time. \ No newline at end of file diff --git a/docs/references/projects/create-smtp-test.md b/docs/references/projects/create-smtp-test.md deleted file mode 100644 index 63cea9d21f..0000000000 --- a/docs/references/projects/create-smtp-test.md +++ /dev/null @@ -1 +0,0 @@ -Send a test email to verify SMTP configuration. \ No newline at end of file diff --git a/docs/references/projects/create.md b/docs/references/projects/create.md deleted file mode 100644 index d502c269ef..0000000000 --- a/docs/references/projects/create.md +++ /dev/null @@ -1 +0,0 @@ -Create a new project. You can create a maximum of 100 projects per account. \ No newline at end of file diff --git a/docs/references/projects/delete-email-template.md b/docs/references/projects/delete-email-template.md deleted file mode 100644 index 332b1d6117..0000000000 --- a/docs/references/projects/delete-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state. \ No newline at end of file diff --git a/docs/references/projects/delete.md b/docs/references/projects/delete.md deleted file mode 100644 index 4a8070c082..0000000000 --- a/docs/references/projects/delete.md +++ /dev/null @@ -1 +0,0 @@ -Delete a project by its unique ID. \ No newline at end of file diff --git a/docs/references/projects/get-email-template.md b/docs/references/projects/get-email-template.md deleted file mode 100644 index 6119a0a183..0000000000 --- a/docs/references/projects/get-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details. \ No newline at end of file diff --git a/docs/references/projects/get.md b/docs/references/projects/get.md deleted file mode 100644 index b7a1165adc..0000000000 --- a/docs/references/projects/get.md +++ /dev/null @@ -1 +0,0 @@ -Get a project by its unique ID. This endpoint allows you to retrieve the project's details, including its name, description, team, region, and other metadata. \ No newline at end of file diff --git a/docs/references/projects/update-auth-status.md b/docs/references/projects/update-auth-status.md deleted file mode 100644 index 5d39ec29c4..0000000000 --- a/docs/references/projects/update-auth-status.md +++ /dev/null @@ -1 +0,0 @@ -Update the status of a specific authentication method. Use this endpoint to enable or disable different authentication methods such as email, magic urls or sms in your project. \ No newline at end of file diff --git a/docs/references/projects/update-email-template.md b/docs/references/projects/update-email-template.md deleted file mode 100644 index d2bf124541..0000000000 --- a/docs/references/projects/update-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates. \ No newline at end of file diff --git a/docs/references/projects/update-mock-numbers.md b/docs/references/projects/update-mock-numbers.md deleted file mode 100644 index 7fa92455c1..0000000000 --- a/docs/references/projects/update-mock-numbers.md +++ /dev/null @@ -1 +0,0 @@ -Update the list of mock phone numbers for testing. Use these numbers to bypass SMS verification in development. \ No newline at end of file diff --git a/docs/references/projects/update-oauth2.md b/docs/references/projects/update-oauth2.md deleted file mode 100644 index 2285135991..0000000000 --- a/docs/references/projects/update-oauth2.md +++ /dev/null @@ -1 +0,0 @@ -Update the OAuth2 provider configurations. Use this endpoint to set up or update the OAuth2 provider credentials or enable/disable providers. \ No newline at end of file diff --git a/docs/references/projects/update-smtp.md b/docs/references/projects/update-smtp.md deleted file mode 100644 index 7d898e1ed1..0000000000 --- a/docs/references/projects/update-smtp.md +++ /dev/null @@ -1 +0,0 @@ -Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails. \ No newline at end of file diff --git a/docs/references/projects/update.md b/docs/references/projects/update.md deleted file mode 100644 index 60c072c477..0000000000 --- a/docs/references/projects/update.md +++ /dev/null @@ -1 +0,0 @@ -Update a project by its unique ID. \ No newline at end of file From 83dc606c1489f6d11eee45c3e94ee4058ac87cf0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 20 May 2026 16:44:10 +0530 Subject: [PATCH 54/97] Use platform enum metadata for SDK specs --- composer.json | 2 +- composer.lock | 17 +- .../Account/MFA/Authenticators/Create.php | 5 +- .../Account/MFA/Authenticators/Delete.php | 5 +- .../Account/MFA/Authenticators/Update.php | 5 +- .../Http/Account/MFA/Challenges/Create.php | 5 +- .../Modules/Avatars/Http/Browsers/Get.php | 6 +- .../Modules/Avatars/Http/CreditCards/Get.php | 6 +- .../Modules/Avatars/Http/Flags/Get.php | 6 +- .../Modules/Avatars/Http/Screenshots/Get.php | 5 +- .../Modules/Console/Http/Resources/Get.php | 5 +- .../Attributes/Relationship/Create.php | 5 +- .../Attributes/Relationship/Update.php | 3 +- .../Databases/Collections/Indexes/Create.php | 5 +- .../Http/Databases/Collections/Usage/Get.php | 10 +- .../Databases/Http/Databases/Usage/Get.php | 12 +- .../Databases/Http/Databases/Usage/XList.php | 12 +- .../Collections/Indexes/Create.php | 5 +- .../DocumentsDB/Collections/Usage/Get.php | 10 +- .../Databases/Http/DocumentsDB/Usage/Get.php | 10 +- .../Http/DocumentsDB/Usage/XList.php | 10 +- .../Tables/Columns/Relationship/Create.php | 5 +- .../Tables/Columns/Relationship/Update.php | 3 +- .../Http/TablesDB/Tables/Indexes/Create.php | 5 +- .../Http/TablesDB/Tables/Usage/Get.php | 10 +- .../Databases/Http/TablesDB/Usage/Get.php | 10 +- .../Databases/Http/TablesDB/Usage/XList.php | 10 +- .../VectorsDB/Collections/Indexes/Create.php | 5 +- .../Http/VectorsDB/Collections/Usage/Get.php | 10 +- .../Databases/Http/VectorsDB/Usage/Get.php | 10 +- .../Databases/Http/VectorsDB/Usage/XList.php | 10 +- .../Http/Deployments/Download/Get.php | 5 +- .../Http/Deployments/Template/Create.php | 5 +- .../Functions/Http/Deployments/Vcs/Create.php | 5 +- .../Functions/Http/Functions/Create.php | 3 +- .../Functions/Http/Functions/Update.php | 3 +- .../Modules/Functions/Http/Usage/Get.php | 12 +- .../Modules/Functions/Http/Usage/XList.php | 12 +- .../Http/Migrations/Appwrite/Create.php | 5 +- .../Http/Migrations/Appwrite/Report/Get.php | 5 +- .../Http/Migrations/Firebase/Create.php | 5 +- .../Http/Migrations/Firebase/Report/Get.php | 5 +- .../Http/Migrations/NHost/Create.php | 5 +- .../Http/Migrations/NHost/Report/Get.php | 5 +- .../Http/Migrations/Supabase/Create.php | 5 +- .../Http/Migrations/Supabase/Report/Get.php | 5 +- .../Modules/Presences/HTTP/Usage/Get.php | 12 +- .../Project/Http/Project/Keys/Create.php | 5 +- .../Http/Project/Keys/Ephemeral/Create.php | 5 +- .../Project/Http/Project/Keys/Update.php | 5 +- .../Proxy/Http/Rules/Redirect/Create.php | 9 +- .../Sites/Http/Deployments/Download/Get.php | 5 +- .../Http/Deployments/Template/Create.php | 5 +- .../Sites/Http/Deployments/Vcs/Create.php | 5 +- .../Platform/Modules/Sites/Http/Usage/Get.php | 12 +- .../Modules/Sites/Http/Usage/XList.php | 12 +- .../Http/Buckets/Files/Preview/Get.php | 7 +- .../Modules/Storage/Http/Usage/Get.php | 12 +- .../Modules/Storage/Http/Usage/XList.php | 12 +- .../Repositories/Detections/Create.php | 3 +- .../Http/Installations/Repositories/XList.php | 3 +- src/Appwrite/SDK/Specification/Format.php | 750 +----------------- .../SDK/Specification/Format/OpenAPI3.php | 150 ++-- .../SDK/Specification/Format/Swagger2.php | 141 ++-- .../Utopia/Response/Model/Attribute.php | 1 + src/Appwrite/Utopia/Response/Model/Column.php | 1 + .../Utopia/Response/Model/HealthStatus.php | 1 + .../Utopia/Response/Model/PlatformBase.php | 1 + 68 files changed, 453 insertions(+), 1016 deletions(-) diff --git a/composer.json b/composer.json index d4472c6868..961eb60e48 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "utopia-php/logger": "0.8.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.*", - "utopia-php/platform": "1.0.0-rc2", + "utopia-php/platform": "dev-feat/action-param-meta", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", diff --git a/composer.lock b/composer.lock index 1d71d75447..379a6e05bf 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": "b645c7da1728536497fe8186b158f9c6", + "content-hash": "87677413cb65fd4688a9a56a896dad08", "packages": [ { "name": "adhocore/jwt", @@ -4773,16 +4773,16 @@ }, { "name": "utopia-php/platform", - "version": "1.0.0-rc2", + "version": "dev-feat/action-param-meta", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "a67e5037007ee7fdca5359ab4577b82917e55452" + "reference": "2e3d957f4f9f672edede820c1815c81976f6606b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/a67e5037007ee7fdca5359ab4577b82917e55452", - "reference": "a67e5037007ee7fdca5359ab4577b82917e55452", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/2e3d957f4f9f672edede820c1815c81976f6606b", + "reference": "2e3d957f4f9f672edede820c1815c81976f6606b", "shasum": "" }, "require": { @@ -4818,9 +4818,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc2" + "source": "https://github.com/utopia-php/platform/tree/feat/action-param-meta" }, - "time": "2026-05-15T06:19:20+00:00" + "time": "2026-05-20T11:39:39+00:00" }, { "name": "utopia-php/pools", @@ -8618,7 +8618,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "utopia-php/http": 5 + "utopia-php/http": 5, + "utopia-php/platform": 20 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php index 93b2b640ff..6aa13e4cd0 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php @@ -6,6 +6,7 @@ use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -17,7 +18,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -77,7 +78,7 @@ class Create extends Action contentType: ContentType::JSON ) ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`') + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`', enum: new Enum(name: 'AuthenticatorType')) ->inject('response') ->inject('project') ->inject('user') diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php index 5765c5bf6e..ce0e8d266e 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php @@ -6,6 +6,7 @@ use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -14,7 +15,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -74,7 +75,7 @@ class Delete extends Action contentType: ContentType::NONE ) ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType')) ->inject('response') ->inject('user') ->inject('dbForProject') 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 f7226d915e..11f044b507 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 @@ -7,6 +7,7 @@ use Appwrite\Auth\MFA\Type; use Appwrite\Auth\MFA\Type\TOTP; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -15,7 +16,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -76,7 +77,7 @@ class Update extends Action contentType: ContentType::JSON ) ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType')) ->param('otp', '', new Text(256), 'Valid verification token.') ->inject('response') ->inject('user') diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php index 285875eb35..27d6f3e088 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php @@ -10,6 +10,7 @@ use Appwrite\Event\Message\Messaging as MessagingMessage; use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -29,7 +30,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Locale\Locale; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Validator\FileName; use Utopia\System\System; @@ -94,7 +95,7 @@ class Create extends Action ]) ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},userId:{userId}') - ->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.') + ->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.', enum: new Enum(name: 'AuthenticationFactor')) ->inject('response') ->inject('dbForProject') ->inject('user') diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php index 637ea647ef..cc30f014bc 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Platform\Action as UtopiaAction; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Range; use Utopia\Validator\WhiteList; @@ -49,7 +50,10 @@ class Get extends Action ], contentType: ContentType::IMAGE_PNG )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.') + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.', enum: new Enum( + name: 'Browser', + map: \array_map(fn (array $browser) => $browser['name'], Config::getParam('avatar-browsers')), + )) ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php index 87357f14c7..3bae32c862 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Platform\Action as UtopiaAction; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Range; use Utopia\Validator\WhiteList; @@ -49,7 +50,10 @@ class Get extends Action ], contentType: ContentType::IMAGE_PNG )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.') + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.', enum: new Enum( + name: 'CreditCard', + map: \array_map(fn (array $creditCard) => $creditCard['name'], Config::getParam('avatar-credit-cards')), + )) ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php index 8230b15f50..ef9f8c0e47 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Platform\Action as UtopiaAction; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Range; use Utopia\Validator\WhiteList; @@ -49,7 +50,10 @@ class Get extends Action ], contentType: ContentType::IMAGE_PNG )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.') + ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.', enum: new Enum( + name: 'Flag', + map: \array_map(fn (array $flag) => $flag['name'], Config::getParam('avatar-flags')), + )) ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php index f33bfa938a..69a4101711 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php @@ -16,6 +16,7 @@ use Utopia\Domains\Domain; use Utopia\Fetch\Client; use Utopia\Image\Image; use Utopia\Platform\Action as UtopiaAction; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -77,12 +78,12 @@ class Get extends Action ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') ->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true') - ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]') + ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]', enum: new Enum(name: 'BrowserPermission')) ->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3') ->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800') ->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600') ->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85') - ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg') + ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg', enum: new Enum(name: 'ImageFormat')) ->inject('response') ->inject('usage') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php index 3e4320716b..f7849e8320 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Console\Http\Resources; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -12,7 +13,7 @@ use Utopia\Database\Database; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Domains\Domain as Domain; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Domain as DomainValidator; @@ -56,7 +57,7 @@ class Get extends Action ->label('abuse-key', 'userId:{userId}, url:{url}') ->label('abuse-time', 60) ->param('value', '', new Text(256), 'Resource value.') - ->param('type', '', new WhiteList(['rules']), 'Resource type.') + ->param('type', '', new WhiteList(['rules']), 'Resource type.', enum: new Enum(name: 'ConsoleResourceType')) ->inject('response') ->inject('dbForPlatform') ->inject('platform') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php index ace48a5c56..f94a60b9de 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php @@ -17,6 +17,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\Boolean; use Utopia\Validator\Nullable; use Utopia\Validator\WhiteList; @@ -70,7 +71,7 @@ class Create extends Action Database::RELATION_MANY_TO_ONE, Database::RELATION_MANY_TO_MANY, Database::RELATION_ONE_TO_MANY - ], true), 'Relation type') + ], true), 'Relation type', enum: new Enum(name: 'RelationshipType')) ->param('twoWay', false, new Boolean(), 'Is Two Way?', true) ->param('key', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Attribute Key.', true, ['dbForProject']) ->param('twoWayKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Two Way Attribute Key.', true, ['dbForProject']) @@ -78,7 +79,7 @@ class Create extends Action Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL - ], true), 'Constraints option', true) + ], true), 'Constraints option', true, enum: new Enum(name: 'RelationMutate')) ->inject('response') ->inject('dbForProject') ->inject('publisherForDatabase') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php index 1e9d38a190..f173afd4b2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php @@ -16,6 +16,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\Nullable; use Utopia\Validator\WhiteList; @@ -69,7 +70,7 @@ class Update extends Action Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL - ], true), 'Constraints option', true) + ], true), 'Constraints option', true, enum: new Enum(name: 'RelationMutate')) ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject']) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index 6c13a5c33c..4e989621f1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -22,6 +22,7 @@ use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; use Utopia\Validator\Nullable; @@ -72,9 +73,9 @@ class Create extends Action ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject']) ->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.', enum: new Enum(name: 'DatabasesIndexType')) ->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject']) - ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true, enum: new Enum(name: 'OrderBy')) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php index bea367af36..2fce1a3906 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php @@ -17,6 +17,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends Action @@ -64,7 +65,14 @@ class Get extends Action ), )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php index 240e7d400c..3060fe9978 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -16,7 +17,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends Action @@ -93,7 +94,14 @@ class Get extends Action ) ]) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php index db73954e7f..17b3ff94db 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Usage; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -14,7 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class XList extends Action @@ -90,7 +91,14 @@ class XList extends Action ) ), ]) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php index 637255f16a..65002e2f30 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php @@ -12,6 +12,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; use Utopia\Validator\Nullable; @@ -58,9 +59,9 @@ class Create extends IndexCreate ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject']) ->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.', enum: new Enum(name: 'DocumentsDBIndexType')) ->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject']) - ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true, enum: new Enum(name: 'OrderBy')) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php index 51dd3c381d..907ba22ae4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php @@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends CollectionUsageGet @@ -54,7 +55,14 @@ class Get extends CollectionUsageGet contentType: ContentType::JSON, )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php index 8373b6bc20..e94106e31e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php @@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends DatabaseUsageGet @@ -51,7 +52,14 @@ class Get extends DatabaseUsageGet ), ]) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php index 16535765ca..4ea8c0e6c3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class XList extends DatabaseUsageXList @@ -48,7 +49,14 @@ class XList extends DatabaseUsageXList contentType: ContentType::JSON ), ]) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php index 414cf03b3d..db8b17473c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php @@ -11,6 +11,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\Boolean; use Utopia\Validator\Nullable; use Utopia\Validator\WhiteList; @@ -60,7 +61,7 @@ class Create extends RelationshipCreate Database::RELATION_MANY_TO_ONE, Database::RELATION_MANY_TO_MANY, Database::RELATION_ONE_TO_MANY - ], true), 'Relation type') + ], true), 'Relation type', enum: new Enum(name: 'RelationshipType')) ->param('twoWay', false, new Boolean(), 'Is Two Way?', true) ->param('key', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Column Key.', true, ['dbForProject']) ->param('twoWayKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Two Way Column Key.', true, ['dbForProject']) @@ -68,7 +69,7 @@ class Create extends RelationshipCreate Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL - ], true), 'Constraints option', true) + ], true), 'Constraints option', true, enum: new Enum(name: 'RelationMutate')) ->inject('response') ->inject('dbForProject') ->inject('publisherForDatabase') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php index 47884eda80..0ec8f4ba07 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php @@ -12,6 +12,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\Nullable; use Utopia\Validator\WhiteList; @@ -60,7 +61,7 @@ class Update extends RelationshipUpdate Database::RELATION_MUTATE_CASCADE, Database::RELATION_MUTATE_RESTRICT, Database::RELATION_MUTATE_SET_NULL - ], true)), 'Constraints option', true) + ], true)), 'Constraints option', true, enum: new Enum(name: 'RelationMutate')) ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject']) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php index 77496fea59..d6c2bf29f3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php @@ -12,6 +12,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; use Utopia\Validator\Nullable; @@ -58,9 +59,9 @@ class Create extends IndexCreate ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).', false, ['dbForProject']) ->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.', enum: new Enum(name: 'TablesDBIndexType')) ->param('columns', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of columns to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' columns are allowed, each 32 characters long.', false, ['dbForProject']) - ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true, enum: new Enum(name: 'OrderBy')) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php index 6976be014c..293db004ee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php @@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends CollectionUsageGet @@ -49,7 +50,14 @@ class Get extends CollectionUsageGet contentType: ContentType::JSON, )) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php index 6f0611e87a..3f3e58dc89 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/Get.php @@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends DatabaseUsageGet @@ -46,7 +47,14 @@ class Get extends DatabaseUsageGet ), ]) ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php index a479693206..33fcaf8130 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Usage/XList.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class XList extends DatabaseUsageXList @@ -43,7 +44,14 @@ class XList extends DatabaseUsageXList contentType: ContentType::JSON ), ]) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php index bba7ee0579..e7e8c8cb1f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php @@ -12,6 +12,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; use Utopia\Validator\Nullable; @@ -58,9 +59,9 @@ class Create extends IndexCreate ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', null, new Key(), 'Index Key.') - ->param('type', null, new WhiteList([Database::INDEX_HNSW_EUCLIDEAN,Database::INDEX_HNSW_DOT, Database::INDEX_HNSW_COSINE, Database::INDEX_OBJECT, Database::INDEX_KEY, Database::INDEX_UNIQUE]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_HNSW_EUCLIDEAN,Database::INDEX_HNSW_DOT, Database::INDEX_HNSW_COSINE, Database::INDEX_OBJECT, Database::INDEX_KEY, Database::INDEX_UNIQUE]), 'Index type.', enum: new Enum(name: 'VectorsDBIndexType')) ->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.') - ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true, enum: new Enum(name: 'OrderBy')) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php index 7e0f79a9f1..fa28028cc1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends CollectionUsageGet @@ -53,7 +54,14 @@ class Get extends CollectionUsageGet contentType: ContentType::JSON, )) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php index 051e2e39fa..725e11e48b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends DatabaseUsageGet @@ -50,7 +51,14 @@ class Get extends DatabaseUsageGet ), ]) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php index d91a5963c4..b2aae774d8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class XList extends DatabaseUsageXList @@ -48,7 +49,14 @@ class XList extends DatabaseUsageXList contentType: ContentType::JSON ), ]) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php index d3e7155dc6..c92e3c1406 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Download; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -12,7 +13,7 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\Validator\WhiteList; @@ -55,7 +56,7 @@ class Get extends Action )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) ->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject']) - ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true, enum: new Enum(name: 'DeploymentDownloadType')) ->inject('response') ->inject('request') ->inject('dbForProject') 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 f18543c60e..87d1282d8b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -6,6 +6,7 @@ use Appwrite\Event\Event; use Appwrite\Event\Message\Build as BuildMessage; use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -19,7 +20,7 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -68,7 +69,7 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag', enum: new Enum(name: 'TemplateReferenceType')) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php index a74fc12593..4a9ae4051f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs; use Appwrite\Event\Event; use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -14,7 +15,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -61,7 +62,7 @@ class Create extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) // TODO: Support tag in future - ->param('type', '', new WhiteList(['branch', 'commit']), 'Type of reference passed. Allowed values are: branch, commit') + ->param('type', '', new WhiteList(['branch', 'commit']), 'Type of reference passed. Allowed values are: branch, commit', enum: new Enum(name: 'VCSReferenceType')) ->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name, commit hash') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 148f0945ac..dcb598a2a4 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -33,6 +33,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Roles; use Utopia\Http\Request; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -89,7 +90,7 @@ class Create extends Base ->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true) ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true, enum: new Enum(name: 'ProjectKeyScopes')) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true) ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index e8713a179d..2c26adea71 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -25,6 +25,7 @@ use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -81,7 +82,7 @@ class Update extends Base ->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true) ->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true) ->param('commands', '', new Text(8192, 0), 'Build Commands.', true) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true, enum: new Enum(name: 'ProjectKeyScopes')) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true) ->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true) ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php index 7016d600cb..def354aa2d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -14,7 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -52,7 +53,14 @@ class Get extends Base ] )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php index 70b7b8e058..1ac77da6f8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -13,7 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -50,7 +51,14 @@ class XList extends Base ) ] )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php index fa700877a1..34bc7ac4f2 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\Appwrite; use Appwrite\Event\Event; use Appwrite\Event\Message\Migration as MigrationMessage; use Appwrite\Event\Publisher\Migration as MigrationPublisher; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -15,7 +16,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\Migration\Destinations\OnDuplicate; use Utopia\Migration\Sources\Appwrite as AppwriteSource; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; @@ -54,7 +55,7 @@ class Create extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(AppwriteSource::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(AppwriteSource::getSupportedResources())), 'List of resources to migrate', enum: new Enum(name: 'AppwriteMigrationResource')) ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') ->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject']) ->param('apiKey', '', new Text(512), 'Source API Key') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Report/Get.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Report/Get.php index 32d8a62ec3..539a4ec57f 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Report/Get.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Report/Get.php @@ -3,13 +3,14 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\Appwrite\Report; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; use Utopia\Migration\Sources\Appwrite as AppwriteSource; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; @@ -46,7 +47,7 @@ class Get extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(AppwriteSource::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(AppwriteSource::getSupportedResources())), 'List of resources to migrate', enum: new Enum(name: 'AppwriteMigrationResource')) ->param('endpoint', '', new URL(), "Source's Appwrite Endpoint") ->param('projectID', '', new Text(512), "Source's Project ID") ->param('key', '', new Text(512), "Source's API Key") diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Create.php index a8347858b4..c6a552985f 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Create.php @@ -6,6 +6,7 @@ use Appwrite\Event\Event; use Appwrite\Event\Message\Migration as MigrationMessage; use Appwrite\Event\Publisher\Migration as MigrationPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -15,7 +16,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Migration\Sources\Appwrite as AppwriteSource; use Utopia\Migration\Sources\Firebase; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; @@ -53,7 +54,7 @@ class Create extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate', enum: new Enum(name: 'FirebaseMigrationResource')) ->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials') ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Report/Get.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Report/Get.php index ef8084795e..8919e2e6d1 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Report/Get.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Firebase/Report/Get.php @@ -3,13 +3,14 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\Firebase\Report; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; use Utopia\Migration\Sources\Firebase; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; @@ -45,7 +46,7 @@ class Get extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate', enum: new Enum(name: 'FirebaseMigrationResource')) ->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials') ->inject('response') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Create.php index fb97b1c16c..c3a299ccd5 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\NHost; use Appwrite\Event\Event; use Appwrite\Event\Message\Migration as MigrationMessage; use Appwrite\Event\Publisher\Migration as MigrationPublisher; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -14,7 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Migration\Sources\Appwrite as AppwriteSource; use Utopia\Migration\Sources\NHost; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; @@ -53,7 +54,7 @@ class Create extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate', enum: new Enum(name: 'NHostMigrationResource')) ->param('subdomain', '', new Text(512), 'Source\'s Subdomain') ->param('region', '', new Text(512), 'Source\'s Region') ->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Report/Get.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Report/Get.php index 964f2dc347..a16d47f53d 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Report/Get.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/NHost/Report/Get.php @@ -3,13 +3,14 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\NHost\Report; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; use Utopia\Migration\Sources\NHost; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; @@ -46,7 +47,7 @@ class Get extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate.') + ->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate.', enum: new Enum(name: 'NHostMigrationResource')) ->param('subdomain', '', new Text(512), 'Source\'s Subdomain.') ->param('region', '', new Text(512), 'Source\'s Region.') ->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret.') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Create.php index 98b33e379d..dc835158ab 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\Supabase; use Appwrite\Event\Event; use Appwrite\Event\Message\Migration as MigrationMessage; use Appwrite\Event\Publisher\Migration as MigrationPublisher; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -14,7 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Migration\Sources\Appwrite as AppwriteSource; use Utopia\Migration\Sources\Supabase; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; @@ -54,7 +55,7 @@ class Create extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate', enum: new Enum(name: 'SupabaseMigrationResource')) ->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint') ->param('apiKey', '', new Text(512), 'Source\'s API Key') ->param('databaseHost', '', new Text(512), 'Source\'s Database Host') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Report/Get.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Report/Get.php index 423e611430..d88fa0e2fb 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Report/Get.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Supabase/Report/Get.php @@ -3,13 +3,14 @@ namespace Appwrite\Platform\Modules\Migrations\Http\Migrations\Supabase\Report; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; use Utopia\Migration\Sources\Supabase; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; @@ -47,7 +48,7 @@ class Get extends Action ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate', enum: new Enum(name: 'SupabaseMigrationResource')) ->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint.') ->param('apiKey', '', new Text(512), 'Source\'s API Key.') ->param('databaseHost', '', new Text(512), 'Source\'s Database Host.') diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Usage/Get.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Usage/Get.php index 636010e765..36b488cfe7 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Usage/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Presences\HTTP\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Action as PlatformAction; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -13,7 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; class Get extends PlatformAction @@ -45,7 +46,14 @@ class Get extends PlatformAction ), ], )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php index eebc0a7067..992b3532c4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Keys; use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -17,7 +18,7 @@ use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Nullable; @@ -64,7 +65,7 @@ class Create extends Base )) ->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform']) ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false, enum: new Enum(name: 'ProjectKeyScopes')) ->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php index 4130effe69..aadf76d74f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Keys\Ephemeral; use Ahc\Jwt\JWT; use Appwrite\Event\Event as QueueEvent; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -13,7 +14,7 @@ use Utopia\Config\Config; use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -58,7 +59,7 @@ class Create extends Base ) ], )) - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false, enum: new Enum(name: 'ProjectKeyScopes')) ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600) ->inject('response') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php index 9193bdbfdf..0d96bcf315 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Keys; use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -16,7 +17,7 @@ use Utopia\Database\Exception\Duplicate; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime; use Utopia\Database\Validator\UID; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Nullable; @@ -60,7 +61,7 @@ class Update extends Base )) ->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform']) ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') - ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false, enum: new Enum(name: 'ProjectKeyScopes')) ->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index e8167b44a0..b6f5185422 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -16,6 +16,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Logger\Log; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Domain as ValidatorDomain; @@ -68,7 +69,13 @@ class Create extends Action ->param('url', null, new URL(), 'Target URL of redirection') ->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection') ->param('resourceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'ID of parent resource.', false, ['dbForProject']) - ->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.') + ->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.', enum: new Enum( + name: 'ProxyResourceType', + map: [ + 'site' => 'Site', + 'function' => 'Function', + ] + )) ->inject('response') ->inject('project') ->inject('publisherForCertificates') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php index 339d059365..0ab998292d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Download; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -12,7 +13,7 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\Validator\WhiteList; @@ -54,7 +55,7 @@ class Get extends Action )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject']) ->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject']) - ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true, enum: new Enum(name: 'DeploymentDownloadType')) ->inject('response') ->inject('request') ->inject('dbForProject') 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 29854d473b..b0d39ad6f9 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -6,6 +6,7 @@ use Appwrite\Event\Event; use Appwrite\Event\Message\Build as BuildMessage; use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -19,7 +20,7 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Boolean; @@ -69,7 +70,7 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag', enum: new Enum(name: 'TemplateReferenceType')) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php index d34b8c4055..d4bf42cffe 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Vcs; use Appwrite\Event\Event; use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -15,7 +16,7 @@ use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -62,7 +63,7 @@ class Create extends Base )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject']) // TODO: Support tag in future - ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type of reference passed. Allowed values are: branch, commit') + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type of reference passed. Allowed values are: branch, commit', enum: new Enum(name: 'VCSReferenceType')) ->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name, commit hash') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php index 85968c7550..73400977d1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -14,7 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -52,7 +53,14 @@ class Get extends Base ] )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject']) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php index 636889f6c0..0066bb644c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Sites\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -13,7 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -50,7 +51,14 @@ class XList extends Base ) ] )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index 68bc2cabae..d160bf0ee3 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Storage\Http\Buckets\Files\Preview; use Appwrite\Extend\Exception; use Appwrite\OpenSSL\OpenSSL; +use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Storage\Config\CacheControl; use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl; use Appwrite\SDK\AuthType; @@ -25,7 +26,7 @@ use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; use Utopia\Image\Image; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Span\Span; use Utopia\Storage\Device; @@ -76,7 +77,7 @@ class Get extends Action ->param('fileId', '', new UID(), 'File ID') ->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true) ->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true) - ->param('gravity', Image::GRAVITY_CENTER, new WhiteList(Image::getGravityTypes()), 'Image crop gravity. Can be one of ' . implode(",", Image::getGravityTypes()), true) + ->param('gravity', Image::GRAVITY_CENTER, new WhiteList(Image::getGravityTypes()), 'Image crop gravity. Can be one of ' . implode(",", Image::getGravityTypes()), true, enum: new Enum(name: 'ImageGravity')) ->param('quality', -1, new Range(-1, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->param('borderWidth', 0, new Range(0, 100), 'Preview image border in pixels. Pass an integer between 0 to 100. Defaults to 0.', true) ->param('borderColor', '', new HexColor(), 'Preview image border color. Use a valid HEX color, no # is needed for prefix.', true) @@ -84,7 +85,7 @@ class Get extends Action ->param('opacity', 1, new Range(0, 1, Range::TYPE_FLOAT), 'Preview image opacity. Only works with images having an alpha channel (like png). Pass a number between 0 to 1.', true) ->param('rotation', 0, new Range(-360, 360), 'Preview image rotation in degrees. Pass an integer between -360 and 360.', true) ->param('background', '', new HexColor(), 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true) - ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true) + ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, enum: new Enum(name: 'ImageFormat')) // NOTE: this is only for the sdk generator and is not used in the action below and is utilised in `resources.php` for `resourceToken`. ->param('token', '', new Text(512), 'File token for accessing this file.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php index 10a603f5df..41ae0dd9ab 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Usage/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Storage\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -13,7 +14,7 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -49,7 +50,14 @@ class Get extends Action ] )) ->param('bucketId', '', new UID(), 'Bucket ID.') - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('project') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php index 04eac21754..b4376208c0 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Usage/XList.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Storage\Http\Usage; use Appwrite\Extend\Exception; +use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -12,7 +13,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -47,7 +48,14 @@ class XList extends Action ) ] )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum( + name: 'UsageRange', + map: [ + '24h' => 'Twenty Four Hours', + '30d' => 'Thirty Days', + '90d' => 'Ninety Days', + ] + )) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php index aa7d7ae95c..4da5699a4e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php @@ -46,6 +46,7 @@ use Utopia\Detector\Detector\Framework; use Utopia\Detector\Detector\Packager; use Utopia\Detector\Detector\Runtime; use Utopia\Detector\Detector\Strategy; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Text; @@ -91,7 +92,7 @@ class Create extends Action )) ->param('installationId', '', new Text(256), 'Installation Id') ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework', enum: new Enum(name: 'VCSDetectionType')) ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) ->inject('gitHub') ->inject('response') diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php index b4172fabdf..0163492233 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -50,6 +50,7 @@ use Utopia\Detector\Detector\Framework; use Utopia\Detector\Detector\Packager; use Utopia\Detector\Detector\Runtime; use Utopia\Detector\Detector\Strategy; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Text; @@ -94,7 +95,7 @@ class XList extends Action ] )) ->param('installationId', '', new Text(256), 'Installation Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework', enum: new Enum(name: 'VCSDetectionType')) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ->inject('gitHub') diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php index 0cbd83cf3f..29ad2d84b8 100644 --- a/src/Appwrite/SDK/Specification/Format.php +++ b/src/Appwrite/SDK/Specification/Format.php @@ -3,7 +3,6 @@ namespace Appwrite\SDK\Specification; use Appwrite\Utopia\Response\Model; -use Utopia\Config\Config; use Utopia\DI\Container; use Utopia\Http\Route; @@ -41,78 +40,6 @@ abstract class Format 'license.url' => '', ]; - /** - * @var list, parameter: string, excludeKeys?: list, exclude?: bool}> - */ - private const array OAUTH_PROVIDER_BLACKLIST = [ - [ - 'namespace' => 'account', - 'methods' => [ - 'createOAuth2Session', - 'createOAuth2Token', - 'updateMagicURLSession' - ], - 'parameter' => 'provider', - 'excludeKeys' => [ - 'mock', - 'mock-unverified' - ], - ], - [ - 'namespace' => 'projects', - 'methods' => [ - 'updateOAuth2' - ], - 'parameter' => 'provider', - 'excludeKeys' => [ - 'mock', - 'mock-unverified' - ], - ], - [ - 'namespace' => 'project', - 'methods' => [ - 'getOAuth2Provider' - ], - 'parameter' => 'providerId', - 'excludeKeys' => [ - 'mock', - 'mock-unverified' - ], - ], - ]; - - /** - * @var list, parameter: string, excludeKeys?: list, exclude?: bool}> - */ - private const array PROVIDER_USAGE_BLACKLIST = [ - [ - 'namespace' => 'users', - 'methods' => [ - 'getUsage' - ], - 'parameter' => 'provider', - 'exclude' => true, /* fully excluded */ - ], - ]; - - /** - * @var list, parameter: string, required?: bool, nullable?: bool}> - */ - private const array REQUEST_PARAMETER_OVERRIDES = [ - [ - 'namespace' => 'project', - 'methods' => [ - 'createWebPlatform', - 'updateWebPlatform', - ], - 'parameter' => 'hostname', - 'required' => true, - ], - ]; - - protected array $enumBlacklist = []; - public function __construct(Container $container, array $services, array $routes, array $models, array $keys, int $authCount, string $platform) { $this->container = $container; @@ -122,32 +49,6 @@ abstract class Format $this->keys = $keys; $this->authCount = $authCount; $this->platform = $platform; - - $this->enumBlacklist = $this->buildEnumBlacklist(); - } - - protected function buildEnumBlacklist(): array - { - $blacklist = []; - - foreach ([...self::OAUTH_PROVIDER_BLACKLIST, ...self::PROVIDER_USAGE_BLACKLIST] as $config) { - foreach ($config['methods'] as $method) { - $entry = [ - 'namespace' => $config['namespace'], - 'method' => $method, - 'parameter' => $config['parameter'], - ]; - if (isset($config['excludeKeys'])) { - $entry['excludeKeys'] = $config['excludeKeys']; - } - if (isset($config['exclude'])) { - $entry['exclude'] = $config['exclude']; - } - $blacklist[] = $entry; - } - } - - return $blacklist; } /** @@ -443,596 +344,6 @@ abstract class Format ]); } - protected function getRequestEnumName(string $service, string $method, string $param): ?string - { - /* `$service` is `$namespace` */ - switch ($service) { - case 'proxy': - switch ($method) { - case 'createRedirectRule': - switch ($param) { - case 'resourceType': - return 'ProxyResourceType'; - } - break; - } - break; - case 'console': - switch ($method) { - case 'getResource': - switch ($param) { - case 'type': - return 'ConsoleResourceType'; - case 'value': - return 'ConsoleResourceValue'; - } - break; - case 'getEmailTemplate': - switch ($param) { - case 'templateId': - return 'ProjectEmailTemplateId'; - case 'locale': - return 'ProjectEmailTemplateLocale'; - } - break; - } - break; - case 'account': - switch ($method) { - case 'createOAuth2Session': - case 'createOAuth2Token': - switch ($param) { - case 'provider': - return 'OAuthProvider'; - } - break; - case 'createMfaAuthenticator': - case 'updateMfaAuthenticator': - case 'deleteMfaAuthenticator': - switch ($param) { - case 'type': - return 'AuthenticatorType'; - } - break; - case 'createMfaChallenge': - switch ($param) { - case 'factor': - return 'AuthenticationFactor'; - } - break; - } - break; - case 'avatars': - switch ($method) { - case 'getBrowser': - return 'Browser'; - case 'getCreditCard': - return 'CreditCard'; - case 'getFlag': - return 'Flag'; - case 'getScreenshot': - switch ($param) { - case 'permissions': - return 'BrowserPermission'; - case 'output': - return 'ImageFormat'; - } - break; - } - break; - case 'databases': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getCollectionUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createRelationshipAttribute': - switch ($param) { - case 'type': - return 'RelationshipType'; - case 'onDelete': - return 'RelationMutate'; - } - break; - case 'updateRelationshipAttribute': - switch ($param) { - case 'onDelete': - return 'RelationMutate'; - } - break; - case 'createIndex': - switch ($param) { - case 'type': - return 'DatabasesIndexType'; - case 'orders': - return 'OrderBy'; - } - } - break; - case 'tablesDB': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getTableUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createRelationshipColumn': - switch ($param) { - case 'type': - return 'RelationshipType'; - case 'onDelete': - return 'RelationMutate'; - } - break; - case 'updateRelationshipColumn': - switch ($param) { - case 'onDelete': - return 'RelationMutate'; - } - break; - case 'createIndex': - switch ($param) { - case 'type': - return 'TablesDBIndexType'; - case 'orders': - return 'OrderBy'; - } - } - break; - case 'documentsDB': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getCollectionUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createIndex': - switch ($param) { - case 'type': - return 'DocumentsDBIndexType'; - case 'orders': - return 'OrderBy'; - } - } - break; - case 'vectorsDB': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getCollectionUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createIndex': - switch ($param) { - case 'type': - return 'VectorsDBIndexType'; - case 'orders': - return 'OrderBy'; - } - } - break; - case 'functions': - switch ($method) { - case 'getUsage': - case 'listUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createExecution': - switch ($param) { - case 'method': - return 'ExecutionMethod'; - } - break; - case 'getDeploymentDownload': - switch ($param) { - case 'type': - return 'DeploymentDownloadType'; - } - break; - case 'createVcsDeployment': - switch ($param) { - case 'type': - return 'VCSReferenceType'; - } - break; - case 'createTemplateDeployment': - switch ($param) { - case 'type': - return 'TemplateReferenceType'; - } - break; - } - break; - case 'sites': - switch ($method) { - case 'getDeploymentDownload': - switch ($param) { - case 'type': - return 'DeploymentDownloadType'; - } - break; - case 'getUsage': - case 'listUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createVcsDeployment': - switch ($param) { - case 'type': - return 'VCSReferenceType'; - } - break; - case 'createTemplateDeployment': - switch ($param) { - case 'type': - return 'TemplateReferenceType'; - } - break; - } - break; - case 'vcs': - switch ($method) { - case 'createRepositoryDetection': - case 'listRepositories': - switch ($param) { - case 'type': - return 'VCSDetectionType'; - } - break; - } - break; - case 'messaging': - switch ($method) { - case 'getUsage': - switch ($param) { - case 'period': - return 'MessagingUsageRange'; - } - break; - case 'createSms': - case 'createPush': - case 'createEmail': - case 'updateSms': - case 'updatePush': - case 'updateEmail': - switch ($param) { - case 'status': - return 'MessageStatus'; - case 'priority': - return 'MessagePriority'; - } - break; - case 'createSmtpProvider': - case 'updateSmtpProvider': - switch ($param) { - case 'encryption': - return 'SmtpEncryption'; - } - break; - } - break; - case 'migrations': - switch ($method) { - case 'createAppwriteMigration': - case 'getAppwriteReport': - switch ($param) { - case 'resources': - return 'AppwriteMigrationResource'; - } - break; - case 'createFirebaseMigration': - case 'getFirebaseReport': - switch ($param) { - case 'resources': - return 'FirebaseMigrationResource'; - } - break; - case 'createSupabaseMigration': - case 'getSupabaseReport': - switch ($param) { - case 'resources': - return 'SupabaseMigrationResource'; - } - break; - case 'createNHostMigration': - case 'getNHostReport': - switch ($param) { - case 'resources': - return 'NHostMigrationResource'; - } - break; - } - break; - case 'project': - switch ($method) { - case 'updateAuthMethod': - switch ($param) { - case 'methodId': - return 'ProjectAuthMethodId'; - } - break; - case 'getPolicy': - switch ($param) { - case 'policyId': - return 'ProjectPolicyId'; - } - break; - case 'getOAuth2Provider': - switch ($param) { - case 'providerId': - return 'ProjectOAuthProviderId'; - } - break; - case 'getEmailTemplate': - case 'updateEmailTemplate': - switch ($param) { - case 'templateId': - return 'ProjectEmailTemplateId'; - case 'locale': - return 'ProjectEmailTemplateLocale'; - } - break; - case 'getUsage': - switch ($param) { - case 'period': - return 'ProjectUsageRange'; - } - break; - case 'updateProtocol': - switch ($param) { - case 'protocolId': - return 'ProjectProtocolId'; - } - break; - case 'updateService': - switch ($param) { - case 'serviceId': - return 'ProjectServiceId'; - } - break; - case 'updateSMTP': - case 'createSMTPTest': - switch ($param) { - case 'secure': - return 'ProjectSMTPSecure'; - } - break; - case 'updateOAuth2Google': - switch ($param) { - case 'prompt': - return 'ProjectOAuth2GooglePrompt'; - } - break; - case 'createKey': - case 'createEphemeralKey': - case 'updateKey': - switch ($param) { - case 'scopes': - return 'ProjectKeyScopes'; - } - break; - } - break; - case 'projects': - switch ($method) { - case 'getEmailTemplate': - case 'updateEmailTemplate': - switch ($param) { - case 'type': - return 'EmailTemplateType'; - case 'locale': - return 'EmailTemplateLocale'; - } - break; - case 'createPlatform': - switch ($param) { - case 'type': - return 'PlatformType'; - } - break; - case 'createSmtpTest': - case 'updateSmtp': - switch ($param) { - case 'secure': - return 'SMTPSecure'; - } - break; - case 'updateOAuth2': - switch ($param) { - case 'provider': - return 'OAuthProvider'; - } - break; - case 'updateAuthStatus': - switch ($param) { - case 'method': - return 'AuthMethod'; - } - break; - case 'updateServiceStatus': - switch ($param) { - case 'service': - return 'ApiService'; - } - break; - } - break; - case 'storage': - switch ($method) { - case 'getUsage': - case 'getBucketUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'getFilePreview': - switch ($param) { - case 'gravity': - return 'ImageGravity'; - case 'output': - return 'ImageFormat'; - } - break; - } - break; - case 'users': - switch ($method) { - case 'getUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - case 'createMfaAuthenticator': - case 'updateMfaAuthenticator': - case 'deleteMfaAuthenticator': - switch ($param) { - case 'type': - return 'AuthenticatorType'; - } - break; - case 'createTarget': - switch ($param) { - case 'providerType': - return 'MessagingProviderType'; - } - break; - case 'createSHAUser': - switch ($param) { - case 'passwordVersion': - return 'PasswordHash'; - } - break; - } - break; - case 'presences': - switch ($method) { - case 'getUsage': - switch ($param) { - case 'range': - return 'UsageRange'; - } - break; - } - break; - } - return null; - } - - public function getRequestEnumKeys(string $service, string $method, string $param): array - { - $values = []; - switch ($service) { - case 'avatars': - switch ($method) { - case 'getBrowser': - $codes = Config::getParam('avatar-browsers'); - foreach ($codes as $code => $value) { - $values[] = $value['name']; - } - return $values; - case 'getCreditCard': - $codes = Config::getParam('avatar-credit-cards'); - foreach ($codes as $code => $value) { - $values[] = $value['name']; - } - return $values; - case 'getFlag': - $codes = Config::getParam('avatar-flags'); - foreach ($codes as $code => $value) { - $values[] = $value['name']; - } - return $values; - } - break; - case 'databases': - case 'documentsDB': - case 'vectorsDB': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getCollectionUsage': - // Range Enum Keys - return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days']; - } - break; - case 'tablesDB': - switch ($method) { - case 'getUsage': - case 'listUsage': - case 'getTableUsage': - // Range Enum Keys - return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days']; - } - break; - case 'proxy': - switch ($method) { - case 'createRedirectRule': - switch ($param) { - case 'statusCode': - return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308']; - case 'resourceType': - return ['Site', 'Function']; - } - break; - } - break; - case 'sites': - case 'functions': - switch ($method) { - case 'getUsage': - case 'listUsage': - // Range Enum Keys - return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days']; - } - break; - case 'users': - switch ($method) { - case 'getUsage': - // Range Enum Keys - if ($param == 'range') { - return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days']; - } - } - break; - case 'storage': - switch ($method) { - case 'getUsage': - case 'getBucketUsage': - // Range Enum Keys - return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days']; - } - break; - case 'project': - switch ($method) { - case 'getUsage': - // Range Enum Keys - return ['One Hour', 'One Day']; - } - break; - } - return $values; - } - protected function shouldEmitDefaultForSchema(mixed $default, array $schema): bool { if (isset($schema['enum'])) { @@ -1046,66 +357,27 @@ abstract class Format return true; } - protected function getRequestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array + protected function getRequestParameterConfig(bool $optional, bool $nullable, mixed $default, string $methodName = '', string $paramName = '') { + $required = !$optional; + + if ( + $paramName === 'hostname' + && \in_array($methodName, ['project.createWebPlatform', 'project.updateWebPlatform'], true) + ) { + $required = true; + } + $config = [ - 'required' => !$optional, + 'required' => $required, 'nullable' => $nullable, ]; - foreach ($this->getRequestParameterOverrides() as $override) { - if ( - $override['namespace'] !== $service - || !\in_array($method, $override['methods'], true) - || $override['parameter'] !== $param - ) { - continue; - } - - if (isset($override['required'])) { - $config['required'] = $override['required']; - } - if (isset($override['nullable'])) { - $config['nullable'] = $override['nullable']; - } - break; - } - $config['emitDefault'] = !$config['required'] && !\is_null($default); return $config; } - /** - * @return list, parameter: string, required?: bool, nullable?: bool}> - */ - private function getRequestParameterOverrides(): array - { - return self::REQUEST_PARAMETER_OVERRIDES; - } - - public function getResponseEnumName(string $model, string $param, ?string $enumSDKName = null): ?string - { - if ($enumSDKName) { - return $enumSDKName; - } - - if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') { - return 'PlatformType'; - } - - if ($param !== 'status') { - return null; - } - - return match (true) { - $model === 'healthStatus' => 'HealthCheckStatus', - str_starts_with($model, 'attribute') => 'AttributeStatus', - str_starts_with($model, 'column') => 'ColumnStatus', - default => null, - }; - } - protected function getNestedModels(Model $model, array &$usedModels): void { foreach ($model->getRules() as $rule) { diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 117fb5e321..20accca8ac 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -17,6 +17,7 @@ use Utopia\Database\Database; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Spatial; +use Utopia\Platform\Enum; use Utopia\Validator; use Utopia\Validator\ArrayList; use Utopia\Validator\Nullable; @@ -412,12 +413,11 @@ class OpenAPI3 extends Format $isNullable = $validator instanceof Nullable; $parameter = $this->getRequestParameterConfig( - $sdk->getNamespace(), - $methodName, - $name, $param['optional'], $isNullable, $param['default'], + $sdk->getNamespace() . '.' . $sdk->getMethodName(), + $name, ); $node = [ @@ -629,51 +629,36 @@ class OpenAPI3 extends Format $node['schema']['x-example'] = $param['example']; } - // Iterate from the blackList. If it matches with the current one, then it is a blackList - // Do not add the enum - $allowed = true; - $excludeKeys = null; - foreach ($this->enumBlacklist as $blacklist) { - if ( - $blacklist['namespace'] == $sdk->getNamespace() - && $blacklist['method'] == $methodName - && $blacklist['parameter'] == $name - ) { - // 'exclude' => true means full exclude - if (isset($blacklist['exclude']) && $blacklist['exclude'] === true) { - $allowed = false; - break; + if ($validator->getType() === 'string') { + $enum = $param['enum'] ?? null; + + if ($enum instanceof Enum) { + $enumValues = \array_values($validator->getList()); + + if (!empty($enum->exclude)) { + $keepIndices = []; + foreach ($enumValues as $index => $value) { + if (!\in_array($value, $enum->exclude, true)) { + $keepIndices[] = $index; + } + } + + $enumValues = \array_values(\array_intersect_key($enumValues, \array_flip($keepIndices))); + $node['description'] = $this->parseDescription($node['description'], $enum->exclude); } - if (isset($blacklist['excludeKeys'])) { - $excludeKeys = $blacklist['excludeKeys']; - } - break; - } - } - if ($allowed && $validator->getType() === 'string') { - $allValues = \array_values($validator->getList()); - $allKeys = $this->getRequestEnumKeys($sdk->getNamespace(), $methodName, $name); - - if ($excludeKeys !== null) { - $keepIndices = []; - foreach ($allValues as $index => $value) { - if (!\in_array($value, $excludeKeys, true)) { - $keepIndices[] = $index; + $enumKeys = []; + if (!empty($enum->map)) { + foreach ($enumValues as $enumValue) { + $enumKeys[] = $enum->map[$enumValue] ?? $enumValue; } } - $enumKeys = \array_values(\array_intersect_key($allKeys, \array_flip($keepIndices))); - $enumValues = \array_values(\array_intersect_key($allValues, \array_flip($keepIndices))); - } else { - $enumKeys = $allKeys; - $enumValues = $allValues; - } - $node['schema']['items']['enum'] = $enumValues; - $node['schema']['items']['x-enum-name'] = $this->getRequestEnumName($sdk->getNamespace(), $methodName, $name); - $node['schema']['items']['x-enum-keys'] = $enumKeys; - if (!empty($excludeKeys)) { - $node['description'] = $this->parseDescription($node['description'], $excludeKeys); + $node['schema']['items']['enum'] = $enumValues; + $node['schema']['items']['x-enum-name'] = $enum->name; + if (!empty($enumKeys)) { + $node['schema']['items']['x-enum-keys'] = $enumKeys; + } } } if ($validator->getType() === 'integer') { @@ -683,51 +668,36 @@ class OpenAPI3 extends Format $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: $validator->getList()[0]; - // Iterate from the blackList. If it matches with the current one, then it is a blackList - // Do not add the enum - $allowed = true; - $excludeKeys = null; - foreach ($this->enumBlacklist as $blacklist) { - if ( - $blacklist['namespace'] == $sdk->getNamespace() - && $blacklist['method'] == $methodName - && $blacklist['parameter'] == $name - ) { - // 'exclude' => true means full exclude - if (isset($blacklist['exclude']) && $blacklist['exclude'] === true) { - $allowed = false; - break; + if ($validator->getType() === 'string') { + $enum = $param['enum'] ?? null; + + if ($enum instanceof Enum) { + $enumValues = \array_values($validator->getList()); + + if (!empty($enum->exclude)) { + $keepIndices = []; + foreach ($enumValues as $index => $value) { + if (!\in_array($value, $enum->exclude, true)) { + $keepIndices[] = $index; + } + } + + $enumValues = \array_values(\array_intersect_key($enumValues, \array_flip($keepIndices))); + $node['description'] = $this->parseDescription($node['description'], $enum->exclude); } - if (isset($blacklist['excludeKeys'])) { - $excludeKeys = $blacklist['excludeKeys']; - } - break; - } - } - if ($allowed && $validator->getType() === 'string') { - $allValues = \array_values($validator->getList()); - $allKeys = $this->getRequestEnumKeys($sdk->getNamespace(), $methodName, $name); - - if ($excludeKeys !== null) { - $keepIndices = []; - foreach ($allValues as $index => $value) { - if (!\in_array($value, $excludeKeys, true)) { - $keepIndices[] = $index; + $enumKeys = []; + if (!empty($enum->map)) { + foreach ($enumValues as $enumValue) { + $enumKeys[] = $enum->map[$enumValue] ?? $enumValue; } } - $enumKeys = \array_values(\array_intersect_key($allKeys, \array_flip($keepIndices))); - $enumValues = \array_values(\array_intersect_key($allValues, \array_flip($keepIndices))); - } else { - $enumKeys = $allKeys; - $enumValues = $allValues; - } - $node['schema']['enum'] = $enumValues; - $node['schema']['x-enum-name'] = $this->getRequestEnumName($sdk->getNamespace(), $methodName, $name); - $node['schema']['x-enum-keys'] = $enumKeys; - if (!empty($excludeKeys)) { - $node['description'] = $this->parseDescription($node['description'], $excludeKeys); + $node['schema']['enum'] = $enumValues; + $node['schema']['x-enum-name'] = $enum->name; + if (!empty($enumKeys)) { + $node['schema']['x-enum-keys'] = $enumKeys; + } } } if ($validator->getType() === 'integer') { @@ -819,7 +789,9 @@ class OpenAPI3 extends Format /// If the enum flag is Set, add the enum values to the body $body['content'][$consumes[0]]['schema']['properties'][$name]['enum'] = $node['schema']['enum']; $body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-name'] = $node['schema']['x-enum-name'] ?? null; - $body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-keys'] = $node['schema']['x-enum-keys']; + if (isset($node['schema']['x-enum-keys'])) { + $body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-keys'] = $node['schema']['x-enum-keys']; + } } if ($node['schema']['x-upload-id'] ?? false) { @@ -1022,15 +994,13 @@ class OpenAPI3 extends Format if ($rule['type'] === 'enum' && !empty($rule['enum'])) { if ($rule['array']) { $output['components']['schemas'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']); - $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null); - if ($enumName) { - $output['components']['schemas'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName; + if (!empty($rule['enumSDKName'])) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $rule['enumSDKName']; } } else { $output['components']['schemas'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']); - $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null); - if ($enumName) { - $output['components']['schemas'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName; + if (!empty($rule['enumSDKName'])) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['x-enum-name'] = $rule['enumSDKName']; } } } diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index f0cd52bf99..66244464a7 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -18,6 +18,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Spatial; use Utopia\Http\Route; +use Utopia\Platform\Enum; use Utopia\Validator; use Utopia\Validator\ArrayList; use Utopia\Validator\Nullable; @@ -109,8 +110,9 @@ class Swagger2 extends Format $additionalMethods = null; if (\is_array($sdk)) { $additionalMethods = $sdk; + $sdkValues = \array_values($sdk); /** @var Method $sdk */ - $sdk = $sdk[0]; + $sdk = $sdkValues[0]; } $consumes = []; @@ -402,12 +404,11 @@ class Swagger2 extends Format $isNullable = $validator instanceof Nullable; $parameter = $this->getRequestParameterConfig( - $sdk->getNamespace(), - $methodName, - $name, $param['optional'], $isNullable, $param['default'], + $sdk->getNamespace() . '.' . $sdk->getMethodName(), + $name, ); $node = [ @@ -595,45 +596,36 @@ class Swagger2 extends Format $node['x-example'] = $param['example']; } - $allowed = true; - $excludeKeys = null; - foreach ($this->enumBlacklist as $blacklist) { - if ($blacklist['namespace'] == $namespace && $blacklist['method'] == $methodName && $blacklist['parameter'] == $name) { - // 'exclude' => true means full exclude - if (isset($blacklist['exclude']) && $blacklist['exclude'] === true) { - $allowed = false; - break; + if ($validator->getType() === 'string') { + $enum = $param['enum'] ?? null; + + if ($enum instanceof Enum) { + $enumValues = \array_values($validator->getList()); + + if (!empty($enum->exclude)) { + $keepIndices = []; + foreach ($enumValues as $index => $value) { + if (!\in_array($value, $enum->exclude, true)) { + $keepIndices[] = $index; + } + } + + $enumValues = \array_values(\array_intersect_key($enumValues, \array_flip($keepIndices))); + $node['description'] = $this->parseDescription($node['description'], $enum->exclude); } - if (isset($blacklist['excludeKeys'])) { - $excludeKeys = $blacklist['excludeKeys']; - } - break; - } - } - if ($allowed && $validator->getType() === 'string') { - $allValues = \array_values($validator->getList()); - $allKeys = $this->getRequestEnumKeys($namespace, $methodName, $name); - - if ($excludeKeys !== null) { - $keepIndices = []; - foreach ($allValues as $index => $value) { - if (!\in_array($value, $excludeKeys, true)) { - $keepIndices[] = $index; + $enumKeys = []; + if (!empty($enum->map)) { + foreach ($enumValues as $enumValue) { + $enumKeys[] = $enum->map[$enumValue] ?? $enumValue; } } - $enumKeys = \array_values(\array_intersect_key($allKeys, \array_flip($keepIndices))); - $enumValues = \array_values(\array_intersect_key($allValues, \array_flip($keepIndices))); - } else { - $enumKeys = $allKeys; - $enumValues = $allValues; - } - $node['items']['enum'] = $enumValues; - $node['items']['x-enum-name'] = $this->getRequestEnumName($namespace, $methodName, $name); - $node['items']['x-enum-keys'] = $enumKeys; - if (!empty($excludeKeys)) { - $node['description'] = $this->parseDescription($node['description'], $excludeKeys); + $node['items']['enum'] = $enumValues; + $node['items']['x-enum-name'] = $enum->name; + if (!empty($enumKeys)) { + $node['items']['x-enum-keys'] = $enumKeys; + } } } if ($validator->getType() === 'integer') { @@ -643,45 +635,36 @@ class Swagger2 extends Format $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: $validator->getList()[0]; - $allowed = true; - $excludeKeys = null; - foreach ($this->enumBlacklist as $blacklist) { - if ($blacklist['namespace'] == $namespace && $blacklist['method'] == $methodName && $blacklist['parameter'] == $name) { - // 'exclude' => true means full exclude - if (isset($blacklist['exclude']) && $blacklist['exclude'] === true) { - $allowed = false; - break; + if ($validator->getType() === 'string') { + $enum = $param['enum'] ?? null; + + if ($enum instanceof Enum) { + $enumValues = \array_values($validator->getList()); + + if (!empty($enum->exclude)) { + $keepIndices = []; + foreach ($enumValues as $index => $value) { + if (!\in_array($value, $enum->exclude, true)) { + $keepIndices[] = $index; + } + } + + $enumValues = \array_values(\array_intersect_key($enumValues, \array_flip($keepIndices))); + $node['description'] = $this->parseDescription($node['description'], $enum->exclude); } - if (isset($blacklist['excludeKeys'])) { - $excludeKeys = $blacklist['excludeKeys']; - } - break; - } - } - if ($allowed && $validator->getType() === 'string') { - $allValues = \array_values($validator->getList()); - $allKeys = $this->getRequestEnumKeys($namespace, $methodName, $name); - - if ($excludeKeys !== null) { - $keepIndices = []; - foreach ($allValues as $index => $value) { - if (!\in_array($value, $excludeKeys, true)) { - $keepIndices[] = $index; + $enumKeys = []; + if (!empty($enum->map)) { + foreach ($enumValues as $enumValue) { + $enumKeys[] = $enum->map[$enumValue] ?? $enumValue; } } - $enumKeys = \array_values(\array_intersect_key($allKeys, \array_flip($keepIndices))); - $enumValues = \array_values(\array_intersect_key($allValues, \array_flip($keepIndices))); - } else { - $enumKeys = $allKeys; - $enumValues = $allValues; - } - $node['enum'] = $enumValues; - $node['x-enum-name'] = $this->getRequestEnumName($namespace, $methodName, $name); - $node['x-enum-keys'] = $enumKeys; - if (!empty($excludeKeys)) { - $node['description'] = $this->parseDescription($node['description'], $excludeKeys); + $node['enum'] = $enumValues; + $node['x-enum-name'] = $enum->name; + if (!empty($enumKeys)) { + $node['x-enum-keys'] = $enumKeys; + } } } if ($validator->getType() === 'integer') { @@ -785,7 +768,9 @@ class Swagger2 extends Format /// If the enum flag is Set, add the enum values to the body $body['schema']['properties'][$name]['enum'] = $node['enum']; $body['schema']['properties'][$name]['x-enum-name'] = $node['x-enum-name'] ?? null; - $body['schema']['properties'][$name]['x-enum-keys'] = $node['x-enum-keys']; + if (isset($node['x-enum-keys'])) { + $body['schema']['properties'][$name]['x-enum-keys'] = $node['x-enum-keys']; + } } if ($parameter['nullable']) { @@ -1000,15 +985,13 @@ class Swagger2 extends Format if ($rule['type'] === 'enum' && !empty($rule['enum'])) { if ($rule['array']) { $output['definitions'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']); - $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null); - if ($enumName) { - $output['definitions'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName; + if (!empty($rule['enumSDKName'])) { + $output['definitions'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $rule['enumSDKName']; } } else { $output['definitions'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']); - $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null); - if ($enumName) { - $output['definitions'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName; + if (!empty($rule['enumSDKName'])) { + $output['definitions'][$model->getType()]['properties'][$name]['x-enum-name'] = $rule['enumSDKName']; } } } diff --git a/src/Appwrite/Utopia/Response/Model/Attribute.php b/src/Appwrite/Utopia/Response/Model/Attribute.php index 35de6bacc5..4e2f3c24fd 100644 --- a/src/Appwrite/Utopia/Response/Model/Attribute.php +++ b/src/Appwrite/Utopia/Response/Model/Attribute.php @@ -28,6 +28,7 @@ class Attribute extends Model 'default' => '', 'example' => 'available', 'enum' => ['available', 'processing', 'deleting', 'stuck', 'failed'], + 'enumSDKName' => 'AttributeStatus', ]) ->addRule('error', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/Column.php b/src/Appwrite/Utopia/Response/Model/Column.php index cae8d1fadb..ae7e33fa54 100644 --- a/src/Appwrite/Utopia/Response/Model/Column.php +++ b/src/Appwrite/Utopia/Response/Model/Column.php @@ -28,6 +28,7 @@ class Column extends Model 'default' => '', 'example' => 'available', 'enum' => ['available', 'processing', 'deleting', 'stuck', 'failed'], + 'enumSDKName' => 'ColumnStatus', ]) ->addRule('error', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/HealthStatus.php b/src/Appwrite/Utopia/Response/Model/HealthStatus.php index 24fb8766ce..13206968cc 100644 --- a/src/Appwrite/Utopia/Response/Model/HealthStatus.php +++ b/src/Appwrite/Utopia/Response/Model/HealthStatus.php @@ -28,6 +28,7 @@ class HealthStatus extends Model 'default' => '', 'example' => 'pass', 'enum' => ['pass', 'fail'], + 'enumSDKName' => 'HealthCheckStatus', ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/PlatformBase.php b/src/Appwrite/Utopia/Response/Model/PlatformBase.php index 1b7ec75e6d..8ca0aef803 100644 --- a/src/Appwrite/Utopia/Response/Model/PlatformBase.php +++ b/src/Appwrite/Utopia/Response/Model/PlatformBase.php @@ -51,6 +51,7 @@ abstract class PlatformBase extends Model 'default' => '', 'example' => Platform::TYPE_WEB, 'enum' => self::getSupportedTypes(), + 'enumSDKName' => 'PlatformType', ]) ; } From cbcf86e362ef271979ee1f7ece03b1bb82b4cf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 20 May 2026 13:49:40 +0200 Subject: [PATCH 55/97] Add missing delete endpoint --- .../Organization/Http/Projects/Delete.php | 93 +++++++++++++++++++ .../Modules/Organization/Services/Http.php | 2 + 2 files changed, 95 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php new file mode 100644 index 0000000000..fc8d5cccfc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/organization/projects/:projectId') + ->desc('Delete organization project') + ->groups(['api', 'organization']) + ->label('scope', 'projects.write') + ->label('audits.event', 'projects.delete') + ->label('audits.resource', 'project/{request.projectId}') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'deleteProject', + description: <<param('projectId', '', new UID(), 'Project unique ID.') + ->inject('response') + ->inject('dbForPlatform') + ->inject('publisherForDeletes') + ->inject('authorization') + ->inject('team') + ->callback($this->action(...)); + } + + public function action( + string $projectId, + Response $response, + Database $dbForPlatform, + DeletePublisher $publisherForDeletes, + Authorization $authorization, + Document $team, + ) { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId()))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB'); + } + + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $project, + )); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Services/Http.php b/src/Appwrite/Platform/Modules/Organization/Services/Http.php index 4a7cd69220..49a8f7d832 100644 --- a/src/Appwrite/Platform/Modules/Organization/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Organization/Services/Http.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Organization\Services; use Appwrite\Platform\Modules\Organization\Http\Init as Init; use Appwrite\Platform\Modules\Organization\Http\Projects\Create as CreateProject; +use Appwrite\Platform\Modules\Organization\Http\Projects\Delete as DeleteProject; use Appwrite\Platform\Modules\Organization\Http\Projects\Get as GetProject; use Appwrite\Platform\Modules\Organization\Http\Projects\Update as UpdateProject; use Appwrite\Platform\Modules\Organization\Http\Projects\XList as ListProjects; @@ -23,5 +24,6 @@ class Http extends Service $this->addAction(ListProjects::getName(), new ListProjects()); $this->addAction(GetProject::getName(), new GetProject()); $this->addAction(UpdateProject::getName(), new UpdateProject()); + $this->addAction(DeleteProject::getName(), new DeleteProject()); } } From 4400eedbae2894c7e8d974f0a6f42290b8058dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 20 May 2026 13:56:47 +0200 Subject: [PATCH 56/97] Add proper test coverage --- .../Services/Organization/ProjectsBase.php | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) diff --git a/tests/e2e/Services/Organization/ProjectsBase.php b/tests/e2e/Services/Organization/ProjectsBase.php index eff7c827a3..4e18050670 100644 --- a/tests/e2e/Services/Organization/ProjectsBase.php +++ b/tests/e2e/Services/Organization/ProjectsBase.php @@ -2,14 +2,123 @@ namespace Tests\E2E\Services\Organization; +use Appwrite\Extend\Exception; use Tests\E2E\Client; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; use Utopia\System\System; trait ProjectsBase { + private static array $cachedOrganization = []; + private static array $cachedProjectData = []; + + /** + * Setup and cache an organization (team) for organization endpoint tests. + */ + protected function setupOrganization(): array + { + if (!empty(self::$cachedOrganization)) { + return self::$cachedOrganization; + } + + $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' => 'Organization Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409], 'Setup organization (team) failed'); + + self::$cachedOrganization = [ + 'teamId' => $team['body']['$id'] ?? $teamId, + ]; + + return self::$cachedOrganization; + } + + protected function getOrganizationHeaders(): array + { + $organization = $this->setupOrganization(); + + return array_merge($this->getHeaders(), [ + 'x-appwrite-organization' => $organization['teamId'], + ]); + } + + /** + * Setup and cache a project created via organization endpoints. + */ + protected function setupOrganizationProject(): array + { + if (!empty(self::$cachedProjectData)) { + return self::$cachedProjectData; + } + + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } + $this->assertEquals(201, $project['headers']['status-code'], 'Setup organization project failed'); + + self::$cachedProjectData = [ + 'projectId' => $project['body']['$id'], + 'teamId' => $this->setupOrganization()['teamId'], + ]; + + return self::$cachedProjectData; + } + public function testCreateProject(): void { + $organization = $this->setupOrganization(); + $teamId = $organization['teamId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Organization Project Test', $response['body']['name']); + $this->assertEquals($teamId, $response['body']['teamId']); + $this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']); + $this->assertArrayHasKey('platforms', $response['body']); + $this->assertArrayHasKey('webhooks', $response['body']); + $this->assertArrayHasKey('keys', $response['body']); + + /** + * Test for FAILURE - missing organization header + */ $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -19,8 +128,357 @@ trait ProjectsBase 'region' => System::getEnv('_APP_REGION', 'default'), ]); + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - empty name + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => '', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testCreateDuplicateProject(): void + { + $organization = $this->setupOrganization(); + $projectId = ID::unique(); + + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => $projectId, + 'name' => 'Original Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + /** + * Test for FAILURE - duplicate project ID + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => $projectId, + 'name' => 'Duplicate Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(409, $response['headers']['status-code']); + $this->assertEquals(409, $response['body']['code']); + $this->assertEquals(Exception::PROJECT_ALREADY_EXISTS, $response['body']['type']); + } + + public function testGetProject(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($projectId, $response['body']['$id']); $this->assertEquals('Organization Project Test', $response['body']['name']); + $this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']); + $this->assertArrayHasKey('platforms', $response['body']); + $this->assertArrayHasKey('webhooks', $response['body']); + $this->assertArrayHasKey('keys', $response['body']); + + /** + * Test for FAILURE - project not found + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - project from different organization + */ + $otherTeam = $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' => 'Other Organization', + ]); + $this->assertContains($otherTeam['headers']['status-code'], [201, 409]); + $otherTeamId = $otherTeam['body']['$id'] ?? $otherTeam['body']['teamId']; + + $otherProject = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], array_merge($this->getHeaders(), [ + 'x-appwrite-organization' => $otherTeamId, + ])), [ + 'projectId' => ID::unique(), + 'name' => 'Other Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + $this->assertEquals(201, $otherProject['headers']['status-code']); + $otherProjectId = $otherProject['body']['$id']; + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $otherProjectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + public function testUpdateProject(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => 'Updated Organization Project', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($projectId, $response['body']['$id']); + $this->assertEquals('Updated Organization Project', $response['body']['name']); + + /** + * Test for FAILURE - project not found + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => 'Should Fail', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - empty name + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => '', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testDeleteProject(): void + { + $organization = $this->setupOrganization(); + + $project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Project To Delete', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $project['headers']['status-code']); + $projectId = $project['body']['$id']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Verify project is actually deleted + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - project not found (already deleted) + */ + $response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + public function testListProjects(): void + { + $organization = $this->setupOrganization(); + $teamId = $organization['teamId']; + + // Create a second project in the same organization + $project2 = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Second Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $project2['headers']['status-code']); + $project2Id = $project2['body']['$id']; + + /** + * Test for SUCCESS - basic list + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + $this->assertGreaterThan(0, $response['body']['total']); + + /** + * Test search queries + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders(), [ + 'search' => 'Second Organization Project', + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, $response['body']['total']); + $this->assertIsArray($response['body']['projects']); + $this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']); + + /** + * Test pagination with limit + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['projects']); + + /** + * Test pagination with offset + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::offset(1)->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + + /** + * Test query by name + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::equal('name', ['Second Organization Project'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, count($response['body']['projects'])); + $this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']); + + /** + * Test cursor pagination + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['projects']); + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + + /** + * Test for FAILURE - invalid cursor + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testListProjectsQuerySelect(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::select(['name'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['projects']); + $this->assertEquals('Organization Project Test', $response['body']['projects'][0]['name']); } } From 91227249b8091ae9aba123fd51ebd5d8fe4e81e7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 20 May 2026 17:47:45 +0530 Subject: [PATCH 57/97] chore: use enum metadata server branch --- composer.json | 2 +- composer.lock | 36 ++++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 961eb60e48..65c5066ad1 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", "utopia-php/queue": "0.18.*", - "utopia-php/servers": "0.4.*", + "utopia-php/servers": "dev-feat-param-enum-metadata as 0.4.0", "utopia-php/registry": "0.5.*", "utopia-php/storage": "2.*", "utopia-php/system": "0.10.*", diff --git a/composer.lock b/composer.lock index 379a6e05bf..cee8d63fa2 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": "87677413cb65fd4688a9a56a896dad08", + "content-hash": "5e4279c99bb870892a68ecc99600e8bb", "packages": [ { "name": "adhocore/jwt", @@ -4777,12 +4777,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "2e3d957f4f9f672edede820c1815c81976f6606b" + "reference": "c490c9aa38d8f0e04061b197fa99489450c2641a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/2e3d957f4f9f672edede820c1815c81976f6606b", - "reference": "2e3d957f4f9f672edede820c1815c81976f6606b", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/c490c9aa38d8f0e04061b197fa99489450c2641a", + "reference": "c490c9aa38d8f0e04061b197fa99489450c2641a", "shasum": "" }, "require": { @@ -4792,7 +4792,7 @@ "utopia-php/cli": "0.23.*", "utopia-php/http": "^2.0@RC", "utopia-php/queue": "0.18.*", - "utopia-php/servers": "0.4.*" + "utopia-php/servers": "dev-feat-param-enum-metadata as 0.4.0" }, "require-dev": { "laravel/pint": "1.2.*", @@ -4820,7 +4820,7 @@ "issues": "https://github.com/utopia-php/platform/issues", "source": "https://github.com/utopia-php/platform/tree/feat/action-param-meta" }, - "time": "2026-05-20T11:39:39+00:00" + "time": "2026-05-20T12:10:54+00:00" }, { "name": "utopia-php/pools", @@ -5094,16 +5094,16 @@ }, { "name": "utopia-php/servers", - "version": "0.4.0", + "version": "dev-feat-param-enum-metadata", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "7db346ef377503efe0acafe0791085270cd9ed70" + "reference": "333e93c5e5c629a1ae059003f570a48bc5a0eeb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70", - "reference": "7db346ef377503efe0acafe0791085270cd9ed70", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/333e93c5e5c629a1ae059003f570a48bc5a0eeb6", + "reference": "333e93c5e5c629a1ae059003f570a48bc5a0eeb6", "shasum": "" }, "require": { @@ -5142,9 +5142,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.4.0" + "source": "https://github.com/utopia-php/servers/tree/feat-param-enum-metadata" }, - "time": "2026-05-05T04:08:30+00:00" + "time": "2026-05-20T11:51:30+00:00" }, { "name": "utopia-php/span", @@ -8615,11 +8615,19 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/servers", + "version": "dev-feat-param-enum-metadata", + "alias": "0.4.0", + "alias_normalized": "0.4.0.0" + } + ], "minimum-stability": "dev", "stability-flags": { "utopia-php/http": 5, - "utopia-php/platform": 20 + "utopia-php/platform": 20, + "utopia-php/servers": 20 }, "prefer-stable": true, "prefer-lowest": false, From 9bcb4d7ca725bd760d24ce8c03bca5830597f074 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 21 May 2026 15:31:05 +0530 Subject: [PATCH 58/97] Clarify renamed PR file filtering --- .../Modules/VCS/Http/GitHub/Authorize/External/Update.php | 1 + src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php | 1 + 2 files changed, 2 insertions(+) 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 86ac0899d1..993740c61a 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 @@ -133,6 +133,7 @@ class Update extends Action $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. ...array_filter(array_column($prFiles, 'previous_filename')) ]; 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 7ce9ece505..e139ff68d8 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -215,6 +215,7 @@ class Create extends Action $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); $providerAffectedFiles = [ ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. ...array_filter(array_column($prFiles, 'previous_filename')) ]; From c1e902c79ce8c9356f0484e6cbf6382f304a6a6d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 21 May 2026 15:32:35 +0530 Subject: [PATCH 59/97] Restore deployments on branch creation --- .../Platform/Modules/VCS/Http/GitHub/Events/Create.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 e139ff68d8..c79df05f8a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -133,7 +133,6 @@ class Create extends Action callable $getProjectDB, array $platform, ) { - $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; $providerBranch = $parsedPayload["branch"] ?? ''; $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; @@ -162,8 +161,8 @@ class Create extends Action Query::limit(100), ])); - // Create new deployment only on push (not committed by us) and not when branch is created or deleted - if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { + // Create new deployment only on push (not committed by us) and not when branch is deleted + if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) { $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', $providerAffectedFiles, false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); } From 01a6d6e4d68cc5c6bb9827b81374f266e3673df8 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 21 May 2026 15:35:44 +0530 Subject: [PATCH 60/97] Inline VCS build trigger checks --- .../Modules/VCS/Http/GitHub/Deployment.php | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 76e0fbc2b9..0978122e02 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -105,11 +105,32 @@ trait Deployment continue; } - if (!$this->isResourceBuildable($resource, $providerBranch, $providerAffectedFiles, $logBase)) { + // Skip deployments when the branch or affected files do not match configured build triggers. + $branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', [])); + if (!$branchTrigger->isValid($providerBranch)) { + Span::add("{$logBase}.build.skipped.reason", 'branch'); Span::add("{$logBase}.build.skipped", 'true'); continue; } + $providerPaths = $resource->getAttribute('providerPaths', []); + if (!empty($providerPaths) && !empty($providerAffectedFiles)) { + $pathTrigger = new BuildTrigger($providerPaths); + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($pathTrigger->isValid($file)) { + $pathMatched = true; + break; + } + } + + if (!$pathMatched) { + Span::add("{$logBase}.build.skipped.reason", 'path'); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); @@ -577,30 +598,4 @@ trait Deployment return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); } - private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool - { - $branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', [])); - if (!$branchTrigger->isValid($providerBranch)) { - Span::add("{$logBase}.build.skipped.reason", 'branch'); - return false; - } - - $providerPaths = $resource->getAttribute('providerPaths', []); - if (!empty($providerPaths) && !empty($providerAffectedFiles)) { - $pathTrigger = new BuildTrigger($providerPaths); - $pathMatched = false; - foreach ($providerAffectedFiles as $file) { - if ($pathTrigger->isValid($file)) { - $pathMatched = true; - break; - } - } - if (!$pathMatched) { - Span::add("{$logBase}.build.skipped.reason", 'path'); - return false; - } - } - - return true; - } } From 268dd501067e2617f8d81840110c9b52ce5b1018 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 21 May 2026 18:43:21 +0530 Subject: [PATCH 61/97] Use Globstar validator directly, remove BuildTrigger wrapper Co-Authored-By: Claude Sonnet 4.6 --- composer.lock | 12 +- .../Modules/VCS/Http/GitHub/Deployment.php | 6 +- src/Appwrite/Vcs/Validator/BuildTrigger.php | 110 ----- tests/unit/Vcs/Validator/BuildTriggerTest.php | 466 ------------------ 4 files changed, 9 insertions(+), 585 deletions(-) delete mode 100644 src/Appwrite/Vcs/Validator/BuildTrigger.php delete mode 100644 tests/unit/Vcs/Validator/BuildTriggerTest.php diff --git a/composer.lock b/composer.lock index a0a687d8ae..07f8594b55 100644 --- a/composer.lock +++ b/composer.lock @@ -5355,16 +5355,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23" + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/b4ee60db4dbae5ffbe53968d01f69b6941251576", + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576", "shasum": "" }, "require": { @@ -5394,9 +5394,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.3" + "source": "https://github.com/utopia-php/validators/tree/0.2.4" }, - "time": "2026-05-14T08:05:44+00:00" + "time": "2026-05-21T12:47:43+00:00" }, { "name": "utopia-php/vcs", diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 0978122e02..260fec77af 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -8,7 +8,7 @@ use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Vcs\Comment; -use Appwrite\Vcs\Validator\BuildTrigger; +use Utopia\Validator\Globstar; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Database; @@ -106,7 +106,7 @@ trait Deployment } // Skip deployments when the branch or affected files do not match configured build triggers. - $branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', [])); + $branchTrigger = new Globstar($resource->getAttribute('providerBranches', [])); if (!$branchTrigger->isValid($providerBranch)) { Span::add("{$logBase}.build.skipped.reason", 'branch'); Span::add("{$logBase}.build.skipped", 'true'); @@ -115,7 +115,7 @@ trait Deployment $providerPaths = $resource->getAttribute('providerPaths', []); if (!empty($providerPaths) && !empty($providerAffectedFiles)) { - $pathTrigger = new BuildTrigger($providerPaths); + $pathTrigger = new Globstar($providerPaths); $pathMatched = false; foreach ($providerAffectedFiles as $file) { if ($pathTrigger->isValid($file)) { diff --git a/src/Appwrite/Vcs/Validator/BuildTrigger.php b/src/Appwrite/Vcs/Validator/BuildTrigger.php deleted file mode 100644 index be8b9c43d1..0000000000 --- a/src/Appwrite/Vcs/Validator/BuildTrigger.php +++ /dev/null @@ -1,110 +0,0 @@ -patterns)) { - return true; - } - - $include = array_filter($this->patterns, fn ($p) => !str_starts_with($p, '!')); - $exclude = array_filter($this->patterns, fn ($p) => str_starts_with($p, '!')); - - if (empty($include)) { - foreach ($exclude as $pattern) { - if ($this->matchGlob($value, substr($pattern, 1))) { - return false; - } - } - return true; - } - - $isSpecific = fn ($pattern) => !str_contains($pattern, '*') && !str_contains($pattern, '?'); - - foreach ($include as $pattern) { - if ($isSpecific($pattern) && $this->matchGlob($value, $pattern)) { - return true; - } - } - - foreach ($exclude as $pattern) { - if ($this->matchGlob($value, substr($pattern, 1))) { - return false; - } - } - - foreach ($include as $pattern) { - if (!$isSpecific($pattern) && $this->matchGlob($value, $pattern)) { - return true; - } - } - - return false; - } - - public function getDescription(): string - { - return 'Value must match a specific inclusion, or a wildcard inclusion not overridden by any exclusion.'; - } - - public function isArray(): bool - { - return false; - } - - public function getType(): string - { - return self::TYPE_STRING; - } - - private function matchGlob(string $subject, string $pattern): bool - { - $regex = ''; - $len = strlen($pattern); - $i = 0; - - while ($i < $len) { - $char = $pattern[$i]; - - if ($char === '*' && isset($pattern[$i + 1]) && $pattern[$i + 1] === '*') { - $prevSlash = $i === 0 || $pattern[$i - 1] === '/'; - $nextSlash = isset($pattern[$i + 2]) && $pattern[$i + 2] === '/'; - - if ($prevSlash && $nextSlash) { - // a/**/b → zero or more intermediate dirs (matches a/b, a/x/b, a/x/y/b) - // **/foo → zero or more leading dirs (matches foo, a/foo, a/b/foo) - $regex .= '(?:.+/)?'; - $i += 3; // consume ** and the trailing / - } else { - // foo/** → everything inside (matches foo/a, foo/a/b) - $regex .= '.*'; - $i += 2; - } - } elseif ($char === '*') { - $regex .= '[^/]*'; // anything except a path separator - $i++; - } elseif ($char === '?') { - $regex .= '[^/]'; // any single character except a path separator - $i++; - } else { - $regex .= preg_quote($char, '~'); - $i++; - } - } - - return (bool) preg_match('~^' . $regex . '$~', $subject); - } -} diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php deleted file mode 100644 index 66aa94abed..0000000000 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ /dev/null @@ -1,466 +0,0 @@ -assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/anything')); - $this->assertTrue($validator->isValid('src/deep/nested/file.php')); - } - - // ------------------------------------------------------------------------- - // Pure inclusion — OR semantics (any one match is enough) - // ------------------------------------------------------------------------- - - public function testSingleExactInclusion(): void - { - $validator = new BuildTrigger(['main']); - $this->assertTrue($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertFalse($validator->isValid('main-extra')); - } - - public function testMultipleExactInclusionsOr(): void - { - $validator = new BuildTrigger(['main', 'develop', 'staging']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); - $this->assertFalse($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('production')); - } - - public function testSingleWildcardInclusion(): void - { - $validator = new BuildTrigger(['feature/*']); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/bar')); - $this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross / - $this->assertFalse($validator->isValid('main')); - } - - public function testWildcardWithDash(): void - { - $validator = new BuildTrigger(['feature/test-*']); - $this->assertTrue($validator->isValid('feature/test-1')); - $this->assertTrue($validator->isValid('feature/test-abc')); - $this->assertFalse($validator->isValid('feature/other')); - $this->assertFalse($validator->isValid('feature/test')); - } - - public function testQuestionMarkWildcard(): void - { - $validator = new BuildTrigger(['v?.?']); - $this->assertTrue($validator->isValid('v1.0')); - $this->assertTrue($validator->isValid('v2.5')); - $this->assertFalse($validator->isValid('v10.0')); // ? matches exactly one char, not two - $this->assertFalse($validator->isValid('v1/0')); // ? does not cross / - } - - public function testQuestionMarkDoesNotCrossSlash(): void - { - $validator = new BuildTrigger(['feature/?']); - $this->assertTrue($validator->isValid('feature/a')); - $this->assertTrue($validator->isValid('feature/z')); - $this->assertFalse($validator->isValid('feature/ab')); // ? matches only one char - $this->assertFalse($validator->isValid('feature/a/b')); // ? does not cross / - $this->assertFalse($validator->isValid('feature/')); - } - - public function testQuestionMarkMixedWithStar(): void - { - $validator = new BuildTrigger(['fix-?.*']); - $this->assertTrue($validator->isValid('fix-1.php')); - $this->assertTrue($validator->isValid('fix-a.js')); - $this->assertFalse($validator->isValid('fix-12.php')); // ? matches only one char - $this->assertFalse($validator->isValid('fix-.php')); // ? requires exactly one char - } - - public function testDoubleWildcardAtEnd(): void - { - $validator = new BuildTrigger(['src/**']); - $this->assertTrue($validator->isValid('src/foo.js')); - $this->assertTrue($validator->isValid('src/a/b/c.js')); - $this->assertTrue($validator->isValid('src/deep/nested/file.php')); - $this->assertFalse($validator->isValid('lib/foo.js')); - } - - public function testDoubleWildcardInMiddle(): void - { - $validator = new BuildTrigger(['a/**/b']); - $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs - $this->assertTrue($validator->isValid('a/x/b')); // one - $this->assertTrue($validator->isValid('a/x/y/b')); // two - $this->assertFalse($validator->isValid('a/b/c')); - $this->assertFalse($validator->isValid('x/a/b')); - } - - public function testDoubleWildcardAtStart(): void - { - $validator = new BuildTrigger(['**/foo']); - $this->assertTrue($validator->isValid('foo')); // zero leading dirs - $this->assertTrue($validator->isValid('a/foo')); // one - $this->assertTrue($validator->isValid('a/b/foo')); // two - $this->assertFalse($validator->isValid('foobar')); - $this->assertFalse($validator->isValid('a/foobar')); - } - - public function testMixedExactAndWildcardInclusions(): void - { - $validator = new BuildTrigger(['main', 'feature/*']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('develop')); - $this->assertFalse($validator->isValid('feature/foo/bar')); - } - - // ------------------------------------------------------------------------- - // Pure exclusion — AND semantics (must not match any exclusion) - // ------------------------------------------------------------------------- - - public function testSingleExactExclusion(): void - { - $validator = new BuildTrigger(['!main']); - $this->assertFalse($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('feature/foo')); - } - - public function testMultipleExactExclusionsAnd(): void - { - $validator = new BuildTrigger(['!main', '!develop']); - $this->assertFalse($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); // neither excluded - $this->assertTrue($validator->isValid('feature/foo')); - } - - public function testWildcardExclusion(): void - { - $validator = new BuildTrigger(['!feature/*']); - $this->assertFalse($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('feature/bar')); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('hotfix/urgent')); // not matched by feature/* - } - - public function testDoubleWildcardExclusion(): void - { - $validator = new BuildTrigger(['!src/**']); - $this->assertFalse($validator->isValid('src/foo.js')); - $this->assertFalse($validator->isValid('src/a/b/c.js')); - $this->assertTrue($validator->isValid('lib/foo.js')); - $this->assertTrue($validator->isValid('main')); - } - - // ------------------------------------------------------------------------- - // Mixed inclusion + exclusion - // ------------------------------------------------------------------------- - - public function testInclusionTakesPrecedenceWhenBothMatch(): void - { - // feature/abc matches both '!feature/*' (exclusion) and 'feature/abc' (inclusion) - // Inclusion is checked first, so it wins - $validator = new BuildTrigger(['!feature/*', 'feature/abc']); - $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins - $this->assertFalse($validator->isValid('feature/xyz')); // only exclusion matches - $this->assertFalse($validator->isValid('main')); // no inclusion matches - } - - public function testInclusionWithNoMatchFails(): void - { - // Inclusions exist but none match — exclusion is irrelevant - $validator = new BuildTrigger(['main', '!develop']); - $this->assertTrue($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); // excluded even if inclusion didn't match - $this->assertFalse($validator->isValid('staging')); // no inclusion match - } - - public function testExclusionBlocksWhenInclusionDoesNotMatch(): void - { - $validator = new BuildTrigger(['feature/*', '!hotfix/*']); - $this->assertTrue($validator->isValid('feature/foo')); // matches inclusion - $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match, also excluded - $this->assertFalse($validator->isValid('main')); // no inclusion match - } - - public function testMultipleInclusionsWithSingleExclusion(): void - { - // feature/wip matches wildcard inclusion feature/* but specific exclusion !feature/wip overrides it - $validator = new BuildTrigger(['main', 'develop', 'feature/*', '!feature/wip']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion overrides wildcard inclusion - $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match - } - - public function testSingleInclusionWithMultipleExclusions(): void - { - // specific exclusions !feature/wip and !feature/experimental override wildcard inclusion feature/** - $validator = new BuildTrigger(['feature/**', '!feature/wip', '!feature/experimental']); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins - $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins - $this->assertFalse($validator->isValid('main')); // no inclusion match - } - - public function testMultipleInclusionsWithMultipleExclusions(): void - { - // specific exclusions override the wildcard inclusion; specific inclusion 'main' is unaffected - $validator = new BuildTrigger(['main', 'feature/**', '!feature/wip', '!feature/experimental']); - $this->assertTrue($validator->isValid('main')); // specific inclusion wins regardless - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins - $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins - $this->assertFalse($validator->isValid('develop')); // no inclusion match - } - - public function testWildcardExclusionOverridesWildcardInclusion(): void - { - // src/** is a broad inclusion; !src/generated/** carves out the generated subtree - $validator = new BuildTrigger(['src/**', '!src/generated/**']); - $this->assertTrue($validator->isValid('src/components/Button.php')); - $this->assertTrue($validator->isValid('src/utils/helper.js')); - $this->assertFalse($validator->isValid('src/generated/Foo.php')); // wildcard exclusion wins - $this->assertFalse($validator->isValid('src/generated/bar/Baz.php')); // wildcard exclusion wins - $this->assertFalse($validator->isValid('lib/other.php')); // no inclusion match - } - - public function testSpecificInclusionOverridesWildcardExclusion(): void - { - // Narrow allowlist with a carve-out: exclude all of feature/* except feature/hotfix/critical - $validator = new BuildTrigger(['feature/hotfix/critical', '!feature/**']); - $this->assertTrue($validator->isValid('feature/hotfix/critical')); // inclusion wins - $this->assertFalse($validator->isValid('feature/foo')); // excluded - $this->assertFalse($validator->isValid('feature/hotfix/other')); // excluded - $this->assertFalse($validator->isValid('main')); // no inclusion match - } - - public function testOnlyExclusionsDefaultToTrueUnlessExcluded(): void - { - $validator = new BuildTrigger(['!main', '!develop']); - $this->assertFalse($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); // passes — no inclusions required - $this->assertTrue($validator->isValid('feature/foo')); - } - - // ------------------------------------------------------------------------- - // Standalone wildcards - // ------------------------------------------------------------------------- - - public function testStarAloneMatchesSingleSegmentOnly(): void - { - // * → ^[^/]*$ — matches any single segment; the [^/]* guard prevents crossing / - $validator = new BuildTrigger(['*']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertFalse($validator->isValid('feature/foo')); // * cannot cross / - $this->assertFalse($validator->isValid('a/b/c')); - } - - public function testDoubleStarAloneMatchesEverything(): void - { - // ** alone → ^.*$ — matches any string including paths with separators - $validator = new BuildTrigger(['**']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('src/a/b/c/d/file.php')); - } - - // ------------------------------------------------------------------------- - // Extension patterns — * scope vs. ** scope - // ------------------------------------------------------------------------- - - public function testStarDotExtMatchesRootLevelOnly(): void - { - // *.php → ^[^/]*\.php$ — root-level only; * cannot cross / - $validator = new BuildTrigger(['*.php']); - $this->assertTrue($validator->isValid('Foo.php')); - $this->assertTrue($validator->isValid('index.php')); - $this->assertFalse($validator->isValid('src/Foo.php')); // * does not cross / - $this->assertFalse($validator->isValid('a/b/Foo.php')); - $this->assertFalse($validator->isValid('Foo.js')); - } - - public function testDoubleStarSlashExtMatchesAnyDepth(): void - { - // **/*.php → ^(?:.+/)?[^/]*\.php$ — (?:.+/)? is optional so root also matches - $validator = new BuildTrigger(['**/*.php']); - $this->assertTrue($validator->isValid('Foo.php')); // root — zero leading dirs - $this->assertTrue($validator->isValid('src/Foo.php')); - $this->assertTrue($validator->isValid('src/components/Foo.php')); - $this->assertTrue($validator->isValid('a/b/c/d/Foo.php')); // four levels deep - $this->assertFalse($validator->isValid('Foo.js')); - $this->assertFalse($validator->isValid('src/Foo.js')); - } - - public function testDirPrefixDoubleStarExtPattern(): void - { - // src/**/*.php → ^src/(?:.+/)?[^/]*\.php$ — scoped to src/ at any depth - $validator = new BuildTrigger(['src/**/*.php']); - $this->assertTrue($validator->isValid('src/Foo.php')); - $this->assertTrue($validator->isValid('src/components/Foo.php')); - $this->assertTrue($validator->isValid('src/a/b/c/Foo.php')); - $this->assertFalse($validator->isValid('Foo.php')); // outside src/ - $this->assertFalse($validator->isValid('lib/Foo.php')); - $this->assertFalse($validator->isValid('src/Foo.js')); - } - - // ------------------------------------------------------------------------- - // Dots as literal characters - // ------------------------------------------------------------------------- - - public function testDotsInPatternAreLiteral(): void - { - // preg_quote escapes . to \. — dots are never regex wildcards - $validator = new BuildTrigger(['release-1.0.0']); - $this->assertTrue($validator->isValid('release-1.0.0')); - $this->assertFalse($validator->isValid('release-1X0Y0')); // X/Y must not satisfy a literal dot - $this->assertFalse($validator->isValid('release-1.0.0-hotfix')); // extra suffix rejected by $ anchor - } - - public function testVersionWildcardBranchPattern(): void - { - // v*.*.* → ^v[^/]*\.[^/]*\.[^/]*$ — dots literal; [^/]* is greedy and can consume dots, - // so v1.2.3.4 also matches (first [^/]* eats "1.2") — documents current behavior - $validator = new BuildTrigger(['v*.*.*']); - $this->assertTrue($validator->isValid('v1.2.3')); - $this->assertTrue($validator->isValid('v10.20.30')); - $this->assertTrue($validator->isValid('v1.2.3.4')); // greedy match — first [^/]* eats "1.2" - $this->assertFalse($validator->isValid('v1.2')); // only two dot-segments; third \.[^/]* fails - $this->assertFalse($validator->isValid('1.2.3')); // missing leading v - $this->assertFalse($validator->isValid('v1/2/3')); // [^/]* cannot cross / - } - - public function testDottedFilenamePattern(): void - { - // *.test.js → ^[^/]*\.test\.js$ — both dots literal; * stays in root segment - $validator = new BuildTrigger(['*.test.js']); - $this->assertTrue($validator->isValid('Button.test.js')); - $this->assertTrue($validator->isValid('App.test.js')); - $this->assertFalse($validator->isValid('ButtonXtestYjs')); // dots must be literal - $this->assertFalse($validator->isValid('src/Button.test.js')); // * does not cross / - $this->assertFalse($validator->isValid('Button.test.ts')); - } - - // ------------------------------------------------------------------------- - // Prefix wildcard - // ------------------------------------------------------------------------- - - public function testPrefixWildcardBranchPattern(): void - { - // main* → ^main[^/]*$ — suffix wildcard; [^/]* cannot cross / - $validator = new BuildTrigger(['main*']); - $this->assertTrue($validator->isValid('main')); // zero trailing chars - $this->assertTrue($validator->isValid('main-extra')); - $this->assertTrue($validator->isValid('mainline')); - $this->assertFalse($validator->isValid('main/branch')); // [^/]* cannot cross / - $this->assertFalse($validator->isValid('develop')); - } - - // ------------------------------------------------------------------------- - // Deep nesting - // ------------------------------------------------------------------------- - - public function testDoubleWildcardInMiddleDeepNesting(): void - { - // a/**/b → ^a/(?:.+/)?b$ — .+ inside (?:.+/)? matches any chars including /, - // so it handles arbitrarily many intermediate directories - $validator = new BuildTrigger(['a/**/b']); - $this->assertTrue($validator->isValid('a/x/y/z/b')); // three intermediate dirs - $this->assertTrue($validator->isValid('a/p/q/r/s/b')); // four intermediate dirs - $this->assertTrue($validator->isValid('a/1/2/3/4/5/b')); // five intermediate dirs - $this->assertFalse($validator->isValid('a/x/y/z/b/extra')); // trailing segment rejected - $this->assertFalse($validator->isValid('prefix/a/x/b')); // leading segment rejected - } - - public function testDoubleWildcardAtStartDeepNesting(): void - { - // **/README.md → ^(?:.+/)?README\.md$ — matches at any depth; $ anchor prevents suffixes - $validator = new BuildTrigger(['**/README.md']); - $this->assertTrue($validator->isValid('README.md')); // zero leading dirs - $this->assertTrue($validator->isValid('docs/README.md')); - $this->assertTrue($validator->isValid('a/b/c/d/README.md')); // four levels deep - $this->assertTrue($validator->isValid('x/y/z/w/v/README.md')); // five levels deep - $this->assertFalse($validator->isValid('a/b/c/README.md.bak')); // $ anchor — no suffix - $this->assertFalse($validator->isValid('a/b/c/README.md/extra')); // trailing segment rejected - } - - // ------------------------------------------------------------------------- - // Real-world path patterns - // ------------------------------------------------------------------------- - - public function testGeneratedFilesAnywhereExclusion(): void - { - // !**/generated/** → ^(?:.+/)?generated/.*$ — excludes generated/ at any depth - $validator = new BuildTrigger(['!**/generated/**']); - $this->assertFalse($validator->isValid('generated/Foo.php')); // root generated/ - $this->assertFalse($validator->isValid('src/generated/Foo.php')); - $this->assertFalse($validator->isValid('src/api/generated/Bar.php')); // deep generated/ - $this->assertFalse($validator->isValid('generated/sub/deep/File.php')); // deep inside root generated/ - $this->assertTrue($validator->isValid('src/components/Button.php')); // not under generated/ - $this->assertTrue($validator->isValid('main')); - } - - public function testMultipleExtensionInclusions(): void - { - // OR semantics: any PHP or JS file triggers; other extensions do not - $validator = new BuildTrigger(['**/*.php', '**/*.js']); - $this->assertTrue($validator->isValid('index.php')); - $this->assertTrue($validator->isValid('src/App.php')); - $this->assertTrue($validator->isValid('index.js')); - $this->assertTrue($validator->isValid('src/components/App.js')); - $this->assertFalse($validator->isValid('styles.css')); - $this->assertFalse($validator->isValid('src/styles.css')); - $this->assertFalse($validator->isValid('README.md')); - } - - // ------------------------------------------------------------------------- - // Named-prefix single-level branch - // ------------------------------------------------------------------------- - - public function testReleaseBranchPattern(): void - { - // release/* → ^release/[^/]*$ — one level only; * stops at the next / - $validator = new BuildTrigger(['release/*']); - $this->assertTrue($validator->isValid('release/1.0')); - $this->assertTrue($validator->isValid('release/hotfix')); - $this->assertTrue($validator->isValid('release/2024-01-15')); - $this->assertFalse($validator->isValid('release/1.0/patch')); // * stops at / - $this->assertFalse($validator->isValid('release')); // missing the /* segment - $this->assertFalse($validator->isValid('main')); - } - - // ------------------------------------------------------------------------- - // Case sensitivity - // ------------------------------------------------------------------------- - - public function testPatternMatchingIsCaseSensitive(): void - { - // preg_match is used without the i flag — matching is always case-sensitive - $branchValidator = new BuildTrigger(['main']); - $this->assertTrue($branchValidator->isValid('main')); - $this->assertFalse($branchValidator->isValid('Main')); - $this->assertFalse($branchValidator->isValid('MAIN')); - - $wildcardValidator = new BuildTrigger(['feature/*']); - $this->assertTrue($wildcardValidator->isValid('feature/foo')); - $this->assertFalse($wildcardValidator->isValid('Feature/foo')); - $this->assertFalse($wildcardValidator->isValid('FEATURE/foo')); - } -} From f62d765bbfd2e54040bfc4377eab39dede1df977 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 21 May 2026 18:46:18 +0530 Subject: [PATCH 62/97] format --- src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 260fec77af..27c4eacba3 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -8,7 +8,6 @@ use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Extend\Exception; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Vcs\Comment; -use Utopia\Validator\Globstar; use Utopia\Config\Config; use Utopia\Console; use Utopia\Database\Database; @@ -23,6 +22,7 @@ use Utopia\DSN\DSN; use Utopia\Span\Span; use Utopia\System\System; use Utopia\Validator\Contains; +use Utopia\Validator\Globstar; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; From 1538927486f903480ab24c83bae1711b595529d6 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 May 2026 12:59:05 +0100 Subject: [PATCH 63/97] chore: migrate to utopia-php/http feat-safe-wildcards Adopts the new safe-wildcard dispatch primitive from utopia-php/http#feat-safe-wildcards. Http::execute() is now the re-entrant dispatch entry point and Http::match() is pure (returns ?RouteMatch). The removed Http::getRoute()/setRoute(), Route::getMatchedPath(), Route::getPathValues() callsites are migrated to the new API. Co-Authored-By: Claude Opus 4.7 --- app/controllers/general.php | 14 +++++++------- app/controllers/mock.php | 2 +- app/controllers/shared/api.php | 12 ++++++------ app/controllers/shared/api/auth.php | 2 +- app/http.php | 4 ++-- app/init/resources/request.php | 4 ++-- composer.json | 2 +- composer.lock | 14 +++++++------- src/Appwrite/GraphQL/Resolvers.php | 10 ++-------- 9 files changed, 29 insertions(+), 35 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index b39c2e2623..4b9c4f81b8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -844,14 +844,14 @@ Http::init() // Only run Router when external domain if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } /* * Request format */ - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $request->setRoute($route); if ($route === null) { @@ -876,7 +876,7 @@ Http::init() } if (version_compare($requestFormat, '1.8.0', '<')) { $dbForProject = $getProjectDB($project); - $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); + $request->addFilter(new RequestV20($dbForProject, $route->resolveParams($request->getURI(), $route->getPath()))); } if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); @@ -1154,7 +1154,7 @@ Http::options() // Only run Router when external domain if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } @@ -1189,7 +1189,7 @@ Http::error() ->inject('authorization') ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $class = \get_class($error); $code = $error->getCode(); $message = $error->getMessage(); @@ -1555,7 +1555,7 @@ Http::get('/robots.txt') $response->text($template->render(false)); } else { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } }); @@ -1589,7 +1589,7 @@ Http::get('/humans.txt') $response->text($template->render(false)); } else { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } }); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 4e92b3482d..9daaa4a35b 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -289,7 +289,7 @@ Http::shutdown() ->action(function (Http $utopia, Response $response, Request $request) { $result = []; - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 6e5167660a..7d0a1882d6 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -99,7 +99,7 @@ Http::init() ->inject('apiKey') ->inject('authorization') ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; if ($route === null) { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); } @@ -495,7 +495,7 @@ Http::init() && ! $user->isPrivileged($roles) && $devKey->isEmpty(); - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; if ($route === null) { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); } @@ -579,12 +579,12 @@ Http::init() $response->setUser($user); $request->setUser($user); - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; if ($route === null) { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); } - $path = $route->getMatchedPath(); + $path = $request->getURI(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB, @@ -623,7 +623,7 @@ Http::init() $useCache = $route->getLabel('cache', false); $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); if ($useCache) { - $route = $utopia->match($request); + $route = $utopia->match($request)->route; $roles = $authorization->getRoles(); $isAppUser = $user->isApp($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; @@ -876,7 +876,7 @@ Http::shutdown() } } - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $requestParams = $route->getParamsValues(); /** diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index db98d97bf5..b1c4cb7538 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -49,7 +49,7 @@ Http::init() } } - $route = $utopia->match($request); + $route = $utopia->match($request)?->route; $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $isAppUser = $user->isApp($authorization->getRoles()); diff --git a/app/http.php b/app/http.php index 6dc415f000..6226026a16 100644 --- a/app/http.php +++ b/app/http.php @@ -539,7 +539,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo $app->run($request, $response); - $route = $app->getRoute(); + $route = $app->match($request)?->route; Span::add('http.path', $route?->getPath() ?? 'unknown'); } catch (\Throwable $th) { Span::error($th); @@ -555,7 +555,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo // All good, user is optional information for logger } - $route = $app->getRoute(); + $route = $app->match($request)?->route; $log = $app->context()->get("log"); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 85d8db3698..8c55eaf3e0 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -596,7 +596,7 @@ return function (Container $context): void { // These endpoints moved from /v1/projects/:projectId/ to /v1/ // When accessed via the old alias path, extract projectId from the URI $deprecatedProjectPathPrefix = '/v1/projects/'; - $route = $utopia->match($request); + $route = $utopia->match($request)?->route; if (!empty($route)) { $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); @@ -1093,7 +1093,7 @@ return function (Container $context): void { if ($project->getId() !== 'console') { $teamInternalId = $project->getAttribute('teamInternalId', ''); } else { - $route = $utopia->match($request); + $route = $utopia->match($request)?->route; $path = ! empty($route) ? $route->getPath() : $request->getURI(); $orgHeader = $request->getHeader('x-appwrite-organization', ''); if (str_starts_with($path, '/v1/projects/:projectId')) { diff --git a/composer.json b/composer.json index 34a0238b7a..082063bb32 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "utopia-php/emails": "0.7.*", "utopia-php/dns": "1.7.*", "utopia-php/dsn": "0.2.1", - "utopia-php/http": "2.0.0-rc1", + "utopia-php/http": "2.0.0-rc2", "utopia-php/fetch": "^1.1", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", diff --git a/composer.lock b/composer.lock index 07f8594b55..79b7db5389 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": "597066d71be48add0c649828d820a505", + "content-hash": "fafd8dc07538185b1753e9c16b622002", "packages": [ { "name": "adhocore/jwt", @@ -4346,16 +4346,16 @@ }, { "name": "utopia-php/http", - "version": "2.0.0-rc1", + "version": "2.0.0-rc2", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "3e3b431d443844c6bf810120dee735f45880856f" + "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f", - "reference": "3e3b431d443844c6bf810120dee735f45880856f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/17f3d5e966ada8a5c041717436f069f269aef2b3", + "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3", "shasum": "" }, "require": { @@ -4396,9 +4396,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/2.0.0-rc1" + "source": "https://github.com/utopia-php/http/tree/2.0.0-rc2" }, - "time": "2026-05-05T15:00:03+00:00" + "time": "2026-05-20T11:13:49+00:00" }, { "name": "utopia-php/image", diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 4471ab53a7..ab98e6df0c 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -342,7 +342,6 @@ class Resolvers $lock->acquire(); - $original = $utopia->getRoute(); try { $request = clone $request; $request->addHeader('x-appwrite-source', 'graphql'); @@ -363,10 +362,9 @@ class Resolvers $resolverResponse->setContentType(Response::CONTENT_TYPE_NULL); $resolverResponse->setSent(false); - $route = $utopia->match($request, fresh: true); - $request->setRoute($route); + $request->setRoute($utopia->match($request)?->route); - $utopia->execute($route, $request, $resolverResponse); + $utopia->execute($request, $resolverResponse); self::mergeResponseSideEffects($resolverResponse, $response); @@ -385,10 +383,6 @@ class Resolvers $reject($e); return; } finally { - if ($original !== null) { - $utopia->setRoute($original); - } - $lock->release(); unset(self::$locks[\spl_object_hash($utopia)]); } From 933fffd763f1f8487f824ad88533adf15d6a6dc9 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 May 2026 13:06:07 +0100 Subject: [PATCH 64/97] Use route template instead of request URI for documentsdb path check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documentsdb/vectorsdb routes are registered with setHttpPath('/v1/documentsdb/...') with no aliases, so getPath() returns a template containing the substring we're matching against — and matches the prior getMatchedPath() semantics without depending on the raw request URI. Co-Authored-By: Claude Opus 4.7 --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7d0a1882d6..42e515d3fd 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -584,7 +584,7 @@ Http::init() throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); } - $path = $request->getURI(); + $path = $route->getPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB, From dd0c8af160f48ab5502a41f4b46bb655748a2b8b Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 May 2026 13:34:24 +0100 Subject: [PATCH 65/97] Use 'route' injection in shutdown hooks The 'route' injection introduced in utopia-php/http feat-safe-wildcards is frame-local and non-nullable inside a matched action's hooks, so the shutdown handlers in api.php and mock.php no longer need to call match() and dereference a nullable result. Co-Authored-By: Claude Opus 4.7 --- app/controllers/mock.php | 7 +++---- app/controllers/shared/api.php | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 9daaa4a35b..00389085af 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -13,6 +13,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\Text; @@ -283,13 +284,11 @@ Http::get('/v1/mock/github/callback') Http::shutdown() ->groups(['mock']) - ->inject('utopia') ->inject('response') - ->inject('request') - ->action(function (Http $utopia, Response $response, Request $request) { + ->inject('route') + ->action(function (Response $response, Route $route) { $result = []; - $route = $utopia->match($request)?->route; $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 42e515d3fd..b7a151c3d2 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -37,6 +37,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Roles; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\Span\Span; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -800,7 +801,7 @@ Http::shutdown() Http::shutdown() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -820,7 +821,7 @@ Http::shutdown() ->inject('bus') ->inject('apiKey') ->inject('mode') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -876,7 +877,6 @@ Http::shutdown() } } - $route = $utopia->match($request)?->route; $requestParams = $route->getParamsValues(); /** From 690c72b7899520ec133ec7e85bff83c49071af89 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 May 2026 13:43:32 +0100 Subject: [PATCH 66/97] Use 'route' injection in remaining api/auth init hooks The 'route' injection is frame-local and non-nullable inside a matched action's hooks. Replaces \$utopia->match() lookups in api.php and auth.php init hooks, drops the dead \$utopia inject from the session shutdown hook, and removes the now-redundant null guards. Co-Authored-By: Claude Opus 4.7 --- app/controllers/shared/api.php | 30 +++++++---------------------- app/controllers/shared/api/auth.php | 7 +++---- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index b7a151c3d2..f2373572b7 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -86,7 +86,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('dbForPlatform') ->inject('dbForProject') @@ -99,11 +99,7 @@ Http::init() ->inject('team') ->inject('apiKey') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { - $route = $utopia->match($request)?->route; - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } + ->action(function (Route $route, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { /** * Handle user authentication and session validation. @@ -478,7 +474,7 @@ Http::init() Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -486,7 +482,7 @@ Http::init() ->inject('timelimit') ->inject('devKey') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { $response->setUser($user); $request->setUser($user); @@ -496,11 +492,6 @@ Http::init() && ! $user->isPrivileged($roles) && $devKey->isEmpty(); - $route = $utopia->match($request)?->route; - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; $closestLimit = null; @@ -557,7 +548,7 @@ Http::init() Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -575,16 +566,11 @@ Http::init() ->inject('platform') ->inject('authorization') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); - $route = $utopia->match($request)?->route; - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - $path = $route->getPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, @@ -624,7 +610,6 @@ Http::init() $useCache = $route->getLabel('cache', false); $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); if ($useCache) { - $route = $utopia->match($request)->route; $roles = $authorization->getRoles(); $isAppUser = $user->isApp($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; @@ -762,12 +747,11 @@ Http::init() */ Http::shutdown() ->groups(['session']) - ->inject('utopia') ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) { + ->action(function (Request $request, Response $response, Document $project, Database $dbForProject) { $sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0; if ($sessionLimit === 0) { diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index b1c4cb7538..4fda054fd6 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -9,6 +9,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\System\System; Http::init() @@ -32,13 +33,13 @@ Http::init() Http::init() ->groups(['auth']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('project') ->inject('geodb') ->inject('user') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) { + ->action(function (Route $route, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -49,8 +50,6 @@ Http::init() } } - $route = $utopia->match($request)?->route; - $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $isAppUser = $user->isApp($authorization->getRoles()); From 035cbacbfb4a555f3b766883bb0ebba5f00184d5 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 May 2026 14:08:32 +0100 Subject: [PATCH 67/97] Use 'params' injection in V20 filter Drops the redundant Route::resolveParams() call. The matched path params are now provided directly via the new frame-local 'params' injection from utopia-php/http, avoiding a second URL parse. Co-Authored-By: Claude Opus 4.7 --- app/controllers/general.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 4b9c4f81b8..219c14774f 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -835,7 +835,8 @@ Http::init() ->inject('authorization') ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { + ->inject('params') + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount, array $params) { /* * Appwrite Router */ @@ -876,7 +877,7 @@ Http::init() } if (version_compare($requestFormat, '1.8.0', '<')) { $dbForProject = $getProjectDB($project); - $request->addFilter(new RequestV20($dbForProject, $route->resolveParams($request->getURI(), $route->getPath()))); + $request->addFilter(new RequestV20($dbForProject, $params)); } if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); From e364082577e2a7621d3a6d0a3c8a1127e913b111 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 22 May 2026 07:26:01 +0100 Subject: [PATCH 68/97] fix(project): cast smtpPort to int in response model --- src/Appwrite/Utopia/Response/Model/Project.php | 4 ++-- tests/e2e/Services/Project/ProjectConsoleClientTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index af2a21d551..c788833e88 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -94,7 +94,7 @@ class Project extends Model ->addRule('smtpPort', [ 'type' => self::TYPE_INTEGER, 'description' => 'SMTP server port', - 'default' => '', + 'default' => 0, 'example' => 25, ]) ->addRule('smtpUsername', [ @@ -225,7 +225,7 @@ class Project extends Model $document->setAttribute('smtpReplyToEmail', $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? ''); // Includes backwards compatibility $document->setAttribute('smtpReplyToName', $smtp['replyToName'] ?? ''); $document->setAttribute('smtpHost', $smtp['host'] ?? ''); - $document->setAttribute('smtpPort', $smtp['port'] ?? ''); + $document->setAttribute('smtpPort', (int) ($smtp['port'] ?? 0)); $document->setAttribute('smtpUsername', $smtp['username'] ?? ''); $document->setAttribute('smtpPassword', ''); // Write-only: never expose the stored value $document->setAttribute('smtpSecure', $smtp['secure'] ?? ''); diff --git a/tests/e2e/Services/Project/ProjectConsoleClientTest.php b/tests/e2e/Services/Project/ProjectConsoleClientTest.php index a4c8b73efc..3328c7602f 100644 --- a/tests/e2e/Services/Project/ProjectConsoleClientTest.php +++ b/tests/e2e/Services/Project/ProjectConsoleClientTest.php @@ -103,7 +103,7 @@ class ProjectConsoleClientTest extends Scope $this->assertSame('', $response['body']['smtpReplyToEmail']); $this->assertSame('', $response['body']['smtpReplyToName']); $this->assertSame('', $response['body']['smtpHost']); - $this->assertSame('', $response['body']['smtpPort']); + $this->assertSame(0, $response['body']['smtpPort']); $this->assertSame('', $response['body']['smtpUsername']); $this->assertSame('', $response['body']['smtpPassword']); $this->assertSame('', $response['body']['smtpSecure']); From cf1623bc91b0351bdcaf662727a00e23e0d55f9b Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 12:27:05 +0530 Subject: [PATCH 69/97] Expose providerBranches and providerPaths in Function and Site response models These fields were already persisted on update but omitted from the response model, causing them to disappear after a page refresh in the console. Co-Authored-By: Harsh Mahajan --- src/Appwrite/Utopia/Response/Model/Func.php | 14 ++++++++++++++ src/Appwrite/Utopia/Response/Model/Site.php | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index 3aea364fe5..fa6040904c 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -182,6 +182,20 @@ class Func extends Model 'default' => false, 'example' => false, ]) + ->addRule('providerBranches', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of branch name patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all branches.', + 'default' => [], + 'example' => ['main', 'feat/*'], + 'array' => true, + ]) + ->addRule('providerPaths', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of file path patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all file changes.', + 'default' => [], + 'example' => ['src/**', '!docs/**'], + 'array' => true, + ]) ->addRule('buildSpecification', [ 'type' => self::TYPE_STRING, 'description' => 'Machine specification for deployment builds.', diff --git a/src/Appwrite/Utopia/Response/Model/Site.php b/src/Appwrite/Utopia/Response/Model/Site.php index 941b6104df..330dd1c777 100644 --- a/src/Appwrite/Utopia/Response/Model/Site.php +++ b/src/Appwrite/Utopia/Response/Model/Site.php @@ -173,6 +173,20 @@ class Site extends Model 'default' => false, 'example' => false, ]) + ->addRule('providerBranches', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of branch name patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all branches.', + 'default' => [], + 'example' => ['main', 'feat/*'], + 'array' => true, + ]) + ->addRule('providerPaths', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of file path patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all file changes.', + 'default' => [], + 'example' => ['src/**', '!docs/**'], + 'array' => true, + ]) ->addRule('buildSpecification', [ 'type' => self::TYPE_STRING, 'description' => 'Machine specification for deployment builds.', From e3768ce8eedd7ec9638ad45ca3671026ead37a3d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 13:03:48 +0530 Subject: [PATCH 70/97] Fix missing platform variables in SMTP test email template The SMTP test email uses email-base-styled.tpl as its base template, which contains {{platform}}, {{logoUrl}}, {{accentColor}}, and social/ legal link placeholders. These were never passed as template variables, causing them to render as literal strings (e.g. "{{platform}} logo"). Inject the platform config and pass the variables to MailMessage, matching the pattern used by OTP and magic-url email flows. Co-Authored-By: Harsh Mahajan --- .../Http/Project/SMTP/Tests/Create.php | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php index 8c87a41475..804964733d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php @@ -69,6 +69,7 @@ class Create extends Action ->inject('response') ->inject('project') ->inject('publisherForMails') + ->inject('platform') ->inject('plan') ->callback($this->action(...)); } @@ -89,6 +90,7 @@ class Create extends Action Response $response, Document $project, MailPublisher $publisherForMails, + array $platform, array $plan ): void { // Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config. @@ -144,14 +146,7 @@ class Create extends Action $template = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-smtp-test.tpl'); $template ->setParam('{{from}}', "{$senderName} ({$senderEmail})") - ->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})") - ->setParam('{{logoUrl}}', $plan['logoUrl'] ?? APP_EMAIL_LOGO_URL) - ->setParam('{{accentColor}}', $plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR) - ->setParam('{{twitterUrl}}', $plan['twitterUrl'] ?? APP_SOCIAL_TWITTER) - ->setParam('{{discordUrl}}', $plan['discordUrl'] ?? APP_SOCIAL_DISCORD) - ->setParam('{{githubUrl}}', $plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE) - ->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL) - ->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL); + ->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})"); foreach ($emails as $email) { $publisherForMails->enqueue(new MailMessage( @@ -171,6 +166,17 @@ class Create extends Action 'senderEmail' => $senderEmail, 'senderName' => $senderName, ], + variables: [ + 'platform' => $platform['platformName'] ?? APP_NAME, + 'logoUrl' => $platform['logoUrl'] ?? APP_EMAIL_LOGO_URL, + 'accentColor' => $platform['accentColor'] ?? APP_EMAIL_ACCENT_COLOR, + 'twitter' => $platform['twitterUrl'] ?? APP_SOCIAL_TWITTER, + 'discord' => $platform['discordUrl'] ?? APP_SOCIAL_DISCORD, + 'github' => $platform['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE, + 'terms' => $platform['termsUrl'] ?? APP_EMAIL_TERMS_URL, + 'privacy' => $platform['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL, + ], + platform: $platform, )); } From 2628cc31fc7aa19150597cd2f2e4d4e45fa9a63f Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 13:12:50 +0530 Subject: [PATCH 71/97] Remove unused $plan injection --- .../Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php index 804964733d..fa35b8d6d5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php @@ -70,7 +70,6 @@ class Create extends Action ->inject('project') ->inject('publisherForMails') ->inject('platform') - ->inject('plan') ->callback($this->action(...)); } @@ -91,7 +90,6 @@ class Create extends Action Document $project, MailPublisher $publisherForMails, array $platform, - array $plan ): void { // Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config. // When inline params are provided they are treated as self-contained — project config is ignored From 4dec4f2ff25302747aae094616bbf4ea64bc32cc Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 15:32:44 +0530 Subject: [PATCH 72/97] Fix organization project API headers and queries --- app/config/cors.php | 1 + src/Appwrite/Utopia/Database/Validator/Queries/Projects.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/config/cors.php b/app/config/cors.php index 0454a24495..8147ec8fe5 100644 --- a/app/config/cors.php +++ b/app/config/cors.php @@ -22,6 +22,7 @@ return [ 'X-Appwrite-Locale', 'X-Appwrite-Mode', 'X-Appwrite-JWT', + 'X-Appwrite-Organization', 'X-Appwrite-Response-Format', 'X-Appwrite-Timeout', 'X-Appwrite-ID', diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php index 50c9d850f3..f56dc35953 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php @@ -7,6 +7,7 @@ class Projects extends Base public const ALLOWED_ATTRIBUTES = [ 'name', 'teamId', + 'status', 'labels', 'search' ]; From e831b3e952b0d87889aea42b4715d4e1e2e93db5 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 15:39:16 +0530 Subject: [PATCH 73/97] Add project status index --- app/config/collections/platform.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 7496b7a9a7..e7d10e73e9 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -376,6 +376,13 @@ $platformCollections = [ 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => ID::custom('_key_status'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['status'], + 'lengths' => [], + 'orders' => [], + ], [ '$id' => ID::custom('_key_pingCount'), 'type' => Database::INDEX_KEY, From 6a4dcd76a79aed79fef6cdc32c17bb13fb7c0a95 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 22 May 2026 15:45:32 +0530 Subject: [PATCH 74/97] Keep organization projects schema unchanged --- app/config/collections/platform.php | 7 ------- .../Utopia/Database/Validator/Queries/Projects.php | 1 - 2 files changed, 8 deletions(-) diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index e7d10e73e9..7496b7a9a7 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -376,13 +376,6 @@ $platformCollections = [ 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], - [ - '$id' => ID::custom('_key_status'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['status'], - 'lengths' => [], - 'orders' => [], - ], [ '$id' => ID::custom('_key_pingCount'), 'type' => Database::INDEX_KEY, diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php index f56dc35953..50c9d850f3 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Projects.php @@ -7,7 +7,6 @@ class Projects extends Base public const ALLOWED_ATTRIBUTES = [ 'name', 'teamId', - 'status', 'labels', 'search' ]; From f1abdde9cf55f9266db83d4d69f36884b2bc149c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 23 May 2026 19:14:59 +0530 Subject: [PATCH 75/97] Replace builds worker logs with span attributes --- .../Modules/Functions/Workers/Builds.php | 102 +++++++++--------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 5aa95d3bf2..1d4a8dd689 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -104,8 +104,6 @@ class Builds extends Action Executor $executor, array $plan ): void { - Console::log('Build action started'); - $payload = $message->getPayload(); if (empty($payload)) { @@ -113,6 +111,8 @@ class Builds extends Action } $type = $payload['type'] ?? ''; + Span::add('build.type', $type); + $resource = new Document($payload['resource'] ?? []); $deployment = new Document($payload['deployment'] ?? []); $template = new Document($payload['template'] ?? []); @@ -124,7 +124,6 @@ class Builds extends Action switch ($type) { case BUILD_TYPE_DEPLOYMENT: case BUILD_TYPE_RETRY: - Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); $this->buildDeployment( $deviceForFunctions, @@ -193,8 +192,6 @@ class Builds extends Action Span::add('deployment.id', $deployment->getId()); Span::add('build.timeout', $timeout); - Console::info('Deployment action started'); - $startTime = DateTime::now(); $durationStart = \microtime(true); @@ -268,7 +265,7 @@ class Builds extends Action $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); } - Console::log('Status marked as processing'); + Span::add('deployment.status', 'processing'); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -359,7 +356,7 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Template cloned'); + Span::add('build.source_size', $deployment->getAttribute('sourceSize')); } } elseif ($isVcsEnabled) { // VCS and VCS+Temaplte @@ -403,8 +400,6 @@ class Builds extends Action throw new \Exception('Unable to clone code repository: ' . $stderr); } - Console::log('Git repository cloned'); - // Local refactoring for function folder with spaces if (str_contains($rootDirectory, ' ')) { $rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory); @@ -478,8 +473,6 @@ class Builds extends Action $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); - - Console::log('Git template pushed'); } $tmpPath = '/tmp/builds/' . $deploymentId; @@ -531,18 +524,17 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Git source uploaded'); + Span::add('build.source_size', $deployment->getAttribute('sourceSize')); $this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } - Console::log('Status marked as building'); - /** Request the executor to build the code... */ $deployment->setAttribute('status', 'building'); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'status' => 'building', ])); + Span::add('deployment.status', 'building'); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -687,11 +679,10 @@ class Builds extends Action } $isCanceled = false; - - Console::log('Runtime creation started'); + $span = Span::current(); Co::join([ - Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, $timeout, &$err, $version) { + Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, $timeout, &$err, $version, $span) { try { if ($version === 'v2') { $command = 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh'; @@ -739,16 +730,18 @@ class Builds extends Action outputDirectory: $outputDirectory ?? '' ); - Console::log('createRuntime finished'); } catch (ExecutorTimeout $error) { - Console::warning('createRuntime timed out'); + $span?->set('build.runtime.timed_out', true); + $span?->set('build.runtime.error_type', $error::class); + $span?->set('build.runtime.error_message', $error->getMessage()); $err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error); } catch (\Throwable $error) { - Console::warning('createRuntime failed'); + $span?->set('build.runtime.error_type', $error::class); + $span?->set('build.runtime.error_message', $error->getMessage()); $err = $error; } }), - Co\go(function () use ($executor, $project, &$deployment, &$response, $dbForProject, $timeout, &$err, $queueForRealtime, &$isCanceled) { + Co\go(function () use ($executor, $project, &$deployment, &$response, $dbForProject, $timeout, &$err, $queueForRealtime, &$isCanceled, $span) { try { $insideSeparation = false; @@ -756,7 +749,7 @@ class Builds extends Action deploymentId: $deployment->getId(), projectId: $project->getId(), timeout: $timeout, - callback: function ($logs) use (&$response, &$err, $dbForProject, &$isCanceled, &$deployment, $queueForRealtime, &$insideSeparation) { + callback: function ($logs) use (&$response, &$err, $dbForProject, &$isCanceled, &$deployment, $queueForRealtime, &$insideSeparation, $span) { if ($isCanceled) { return; } @@ -767,7 +760,7 @@ class Builds extends Action if ($deployment->getAttribute('status') === 'canceled') { $isCanceled = true; - Console::info('Ignoring realtime logs because build has been canceled'); + $span?->set('build.logs.ignored_reason', 'canceled'); return; } @@ -836,9 +829,10 @@ class Builds extends Action } } ); - Console::warning('listLogs finished'); + $span?->set('build.logs.finished', true); } catch (\Throwable $error) { - Console::warning('listLogs failed'); + $span?->set('build.logs.error_type', $error::class); + $span?->set('build.logs.error_message', $error->getMessage()); if (empty($err)) { $err = $error; } @@ -846,8 +840,6 @@ class Builds extends Action }), ]); - Console::log('Runtime creation finished'); - $latestDeployment = $dbForProject->getDocument('deployments', $deploymentId); if ($latestDeployment->getAttribute('status') === 'canceled') { $this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime); @@ -870,6 +862,8 @@ class Builds extends Action $deployment->setAttribute('buildPath', $response['path']); $deployment->setAttribute('buildSize', $response['size']); $deployment->setAttribute('totalSize', $deployment->getAttribute('buildSize', 0) + $deployment->getAttribute('sourceSize', 0)); + Span::add('build.size', $deployment->getAttribute('buildSize')); + Span::add('build.total_size', $deployment->getAttribute('totalSize')); $logs = ''; foreach ($response['output'] as $log) { @@ -908,8 +902,8 @@ class Builds extends Action $deployment->setAttribute('adapter', $detection->getName()); $deployment->setAttribute('fallbackFile', $detection->getFallbackFile() ?? ''); - - Console::log('Adapter detected'); + Span::add('build.adapter', $deployment->getAttribute('adapter')); + Span::add('build.fallback_file', $deployment->getAttribute('fallbackFile')); } elseif ($adapter === 'ssr' && $detection->getName() === 'static') { throw new \Exception('Adapter mismatch. Detected: ' . $detection->getName() . ' does not match with the set adapter: ' . $adapter); } @@ -927,8 +921,6 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Build details stored'); - $this->afterBuildSuccess($queueForRealtime, $dbForProject, $deployment, $runtime, $adapter); $logs = $deployment->getAttribute('buildLogs', ''); @@ -942,8 +934,7 @@ class Builds extends Action 'buildLogs' => $deployment->getAttribute('buildLogs'), 'status' => 'ready', ])); - - Console::log('Status marked as ready'); + Span::add('deployment.status', 'ready'); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -969,7 +960,7 @@ class Builds extends Action if ($currentActiveStartTime < $deploymentStartTime) { $activateBuild = true; } else { - Console::info('Skipping auto-activation as current deployment is more recent'); + Span::add('build.auto_activation.skipped_reason', 'current_deployment_newer'); } } } else { @@ -1031,7 +1022,7 @@ class Builds extends Action break; } - Console::log('Deployment activated'); + Span::add('build.activated', true); } $this->afterDeploymentSuccess( @@ -1099,7 +1090,7 @@ class Builds extends Action ])); }, $queries); - Console::log('Preview rule created'); + Span::add('build.preview_rule_created', true); } } @@ -1109,6 +1100,7 @@ class Builds extends Action 'buildEndedAt' => $endTime, 'buildDuration' => \intval(\ceil($durationEnd - $durationStart)), ])); + Span::add('build.duration', $deployment->getAttribute('buildDuration')); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); @@ -1119,8 +1111,6 @@ class Builds extends Action return; } - Console::log('Build duration updated'); - /** Update function schedule */ // Inform scheduler if function is still active @@ -1144,23 +1134,21 @@ class Builds extends Action deploymentId: $deployment->getId(), )); - Console::log('Site screenshot queued'); + Span::add('build.screenshot_queued', true); } - - Console::info('Deployment action finished'); } catch (\Throwable $th) { - Console::warning('Build failed:'); - Console::error($th->getMessage()); - Console::error($th->getFile()); - Console::error($th->getLine()); - Console::error($th->getTraceAsString()); - if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') { $this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime); return; } + Span::add('build.error.stage', 'deployment'); + Span::add('build.error.type', $th::class); + Span::add('build.error.message', $th->getMessage()); + Span::add('build.error.file', $th->getFile()); + Span::add('build.error.line', $th->getLine()); + // Color message red $message = $th->getMessage(); if (! \str_contains($message, '')) { @@ -1182,6 +1170,8 @@ class Builds extends Action $deployment->setAttribute('buildEndedAt', $endTime); $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); $deployment->setAttribute('status', 'failed'); + Span::add('deployment.status', 'failed'); + Span::add('build.duration', $deployment->getAttribute('buildDuration')); $deployment->setAttribute('buildLogs', $message); $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ @@ -1200,7 +1190,7 @@ class Builds extends Action ->trigger(); if ($isVcsEnabled) { - $this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); + $this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform, true); } } finally { $queueForRealtime @@ -1360,7 +1350,8 @@ class Builds extends Action Database $dbForProject, Database $dbForPlatform, Realtime $queueForRealtime, - array $platform + array $platform, + bool $secondaryError = false ): void { $deployment = new Document(); @@ -1456,9 +1447,14 @@ class Builds extends Action } } } catch (\Throwable $th) { - Console::warning('Git action failed:'); - Console::warning($th->getMessage()); - Console::warning($th->getTraceAsString()); + $span = Span::current(); + $errorPrefix = $secondaryError ? 'build.error.secondary' : 'build.git_action.error'; + $span?->set("{$errorPrefix}.stage", 'git_action'); + $span?->set("{$errorPrefix}.status", $status); + $span?->set("{$errorPrefix}.type", $th::class); + $span?->set("{$errorPrefix}.message", $th->getMessage()); + $span?->set("{$errorPrefix}.file", $th->getFile()); + $span?->set("{$errorPrefix}.line", $th->getLine()); $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); @@ -1477,7 +1473,7 @@ class Builds extends Action private function cancelDeployment(string $deploymentId, Database $dbForProject, Realtime $queueForRealtime) { - Console::info('Build has been canceled'); + Span::add('deployment.status', 'canceled'); $deployment = $dbForProject->getDocument('deployments', $deploymentId); From 27370b6acbd53ad66d64994be9018e81a12ac43c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 18 May 2026 03:27:34 +0000 Subject: [PATCH 76/97] feat!: rename ACTIVITY_TYPE_* constants to ACTOR_TYPE_* Renames the actor identity constants used by the audit/auth flow: - ACTIVITY_TYPE_USER -> ACTOR_TYPE_USER ('user') - ACTIVITY_TYPE_ADMIN -> ACTOR_TYPE_ADMIN ('admin') - ACTIVITY_TYPE_GUEST -> ACTOR_TYPE_GUEST ('guest') - ACTIVITY_TYPE_KEY_PROJECT -> ACTOR_TYPE_KEY_PROJECT ('keyProject') - ACTIVITY_TYPE_KEY_ACCOUNT -> ACTOR_TYPE_KEY_ACCOUNT ('keyAccount') - ACTIVITY_TYPE_KEY_ORGANIZATION -> ACTOR_TYPE_KEY_ORGANIZATION ('keyOrganization') Values are unchanged. Call sites updated in: - app/controllers/shared/api.php - src/Appwrite/Platform/Workers/Audits.php Audit payload key rename (userType -> actorType) and utopia-php/audit bump will land in a follow-up PR. BREAKING CHANGE: ACTIVITY_TYPE_* global constants are removed. Any downstream extension or plugin importing those names must be updated to the ACTOR_TYPE_* equivalents. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/controllers/shared/api.php | 14 +++++++------- app/init/constants.php | 14 +++++++------- src/Appwrite/Platform/Workers/Audits.php | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index f2373572b7..2ee601f685 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -186,7 +186,7 @@ Http::init() $user = new User([ '$id' => '', 'status' => true, - 'type' => ACTIVITY_TYPE_KEY_PROJECT, + 'type' => ACTOR_TYPE_KEY_PROJECT, 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => $apiKey->getName(), @@ -258,9 +258,9 @@ Http::init() $userClone = clone $user; $userClone->setAttribute('type', match ($apiKey->getType()) { - API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT, - API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT, - default => ACTIVITY_TYPE_KEY_ORGANIZATION, + API_KEY_STANDARD => ACTOR_TYPE_KEY_PROJECT, + API_KEY_ACCOUNT => ACTOR_TYPE_KEY_ACCOUNT, + default => ACTOR_TYPE_KEY_ORGANIZATION, }); $auditContext->user = $userClone; } @@ -602,7 +602,7 @@ Http::init() $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. if (empty($user->getAttribute('type'))) { - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER); } $auditContext->user = $userClone; } @@ -913,7 +913,7 @@ Http::shutdown() $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. if (empty($user->getAttribute('type'))) { - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER); } $auditContext->user = $userClone; } elseif ($auditContext->user === null || $auditContext->user->isEmpty()) { @@ -928,7 +928,7 @@ Http::shutdown() $user = new User([ '$id' => '', 'status' => true, - 'type' => ACTIVITY_TYPE_GUEST, + 'type' => ACTOR_TYPE_GUEST, 'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => 'Guest', diff --git a/app/init/constants.php b/app/init/constants.php index b271b56a14..4bb607e2fb 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -159,14 +159,14 @@ const SESSION_PROVIDER_TOKEN = 'token'; const SESSION_PROVIDER_SERVER = 'server'; /** - * Activity associated with user or the app. + * Actor that performed the request (user, admin, guest, or API key). */ -const ACTIVITY_TYPE_USER = 'user'; -const ACTIVITY_TYPE_ADMIN = 'admin'; -const ACTIVITY_TYPE_GUEST = 'guest'; -const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject'; -const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount'; -const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization'; +const ACTOR_TYPE_USER = 'user'; +const ACTOR_TYPE_ADMIN = 'admin'; +const ACTOR_TYPE_GUEST = 'guest'; +const ACTOR_TYPE_KEY_PROJECT = 'keyProject'; +const ACTOR_TYPE_KEY_ACCOUNT = 'keyAccount'; +const ACTOR_TYPE_KEY_ORGANIZATION = 'keyOrganization'; /** * MFA diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index f6b0345381..07d91ce009 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -91,7 +91,7 @@ class Audits extends Action $actorUserEmail = $impersonatorUserId ? $user->getAttribute('impersonatorUserEmail', '') : $user->getAttribute('email', ''); - $userType = $user->getAttribute('type', ACTIVITY_TYPE_USER); + $userType = $user->getAttribute('type', ACTOR_TYPE_USER); // Create event data $eventData = [ From 5f1e537e5355f17239d03bb4c1772acb94fa15c6 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 18 May 2026 04:55:01 +0000 Subject: [PATCH 77/97] chore: bump utopia-php/audit to ^2.4 --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 082063bb32..3ddd6ae119 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "utopia-php/abuse": "1.3.*", "utopia-php/agents": "1.2.*", "utopia-php/analytics": "0.15.*", - "utopia-php/audit": "2.3.*", + "utopia-php/audit": "^2.4", "utopia-php/auth": "0.5.*", "utopia-php/cache": "^3.0", "utopia-php/cli": "0.23.*", diff --git a/composer.lock b/composer.lock index 79b7db5389..661558b63c 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": "fafd8dc07538185b1753e9c16b622002", + "content-hash": "b092fffec11494aea10b0c823b7837b8", "packages": [ { "name": "adhocore/jwt", @@ -3510,16 +3510,16 @@ }, { "name": "utopia-php/audit", - "version": "2.3.2", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3" + "reference": "eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3", - "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6", + "reference": "eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6", "shasum": "" }, "require": { @@ -3554,9 +3554,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/2.3.2" + "source": "https://github.com/utopia-php/audit/tree/2.4.1" }, - "time": "2026-05-14T04:00:37+00:00" + "time": "2026-05-20T06:25:45+00:00" }, { "name": "utopia-php/auth", From ce31a4f33621b5cc947be482976fdddb1a344eb0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 18 May 2026 05:34:54 +0000 Subject: [PATCH 78/97] feat!: rename User::ROLE_APPS to ROLE_KEYS and isApp() to isKey() --- app/config/roles.php | 2 +- app/controllers/api/graphql.php | 2 +- app/controllers/shared/api.php | 12 +++---- app/controllers/shared/api/auth.php | 2 +- app/realtime.php | 2 +- src/Appwrite/Auth/Key.php | 6 ++-- .../Documents/Attribute/Decrement.php | 2 +- .../Documents/Attribute/Increment.php | 2 +- .../Collections/Documents/Create.php | 2 +- .../Collections/Documents/Delete.php | 2 +- .../Databases/Collections/Documents/Get.php | 2 +- .../Collections/Documents/Update.php | 2 +- .../Collections/Documents/Upsert.php | 2 +- .../Databases/Collections/Documents/XList.php | 2 +- .../Transactions/Operations/Create.php | 2 +- .../Http/Databases/Transactions/Update.php | 2 +- .../Functions/Http/Executions/Create.php | 2 +- .../Modules/Functions/Http/Executions/Get.php | 2 +- .../Functions/Http/Executions/XList.php | 2 +- .../Storage/Http/Buckets/Files/Create.php | 2 +- .../Storage/Http/Buckets/Files/Delete.php | 2 +- .../Http/Buckets/Files/Download/Get.php | 2 +- .../Storage/Http/Buckets/Files/Get.php | 2 +- .../Http/Buckets/Files/Preview/Get.php | 2 +- .../Storage/Http/Buckets/Files/Push/Get.php | 2 +- .../Storage/Http/Buckets/Files/Update.php | 4 +-- .../Storage/Http/Buckets/Files/View/Get.php | 2 +- .../Storage/Http/Buckets/Files/XList.php | 2 +- .../Modules/Teams/Http/Memberships/Create.php | 2 +- .../Modules/Teams/Http/Memberships/Get.php | 2 +- .../Modules/Teams/Http/Memberships/Update.php | 2 +- .../Modules/Teams/Http/Memberships/XList.php | 2 +- .../Modules/Teams/Http/Teams/Create.php | 2 +- .../Http/Tokens/Buckets/Files/Action.php | 2 +- .../Utopia/Database/Documents/User.php | 10 +++--- src/Appwrite/Utopia/Request.php | 2 +- src/Appwrite/Utopia/Response.php | 2 +- tests/unit/Auth/KeyTest.php | 16 +++++----- .../Utopia/Database/Documents/UserTest.php | 32 +++++++++---------- 39 files changed, 73 insertions(+), 73 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index abb8d4481f..d5ae7c1331 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -150,7 +150,7 @@ return [ 'label' => 'Owner', 'scopes' => \array_merge($member, $admins), ], - User::ROLE_APPS => [ + User::ROLE_KEYS => [ 'label' => 'Applications', 'scopes' => ['global', 'health.read', 'graphql'], ], diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 9ec2479749..4a509aefdd 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -39,7 +39,7 @@ Http::init() if ( array_key_exists('graphql', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 2ee601f685..494bd3da28 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -175,8 +175,8 @@ Http::init() $role = $apiKey->getRole(); $scopes = $apiKey->getScopes(); - // Handle special app role case - if ($apiKey->getRole() === User::ROLE_APPS) { + // Handle special key role case + if ($apiKey->getRole() === User::ROLE_KEYS) { // Disable authorization checks for project API keys // Dynamic supported for backwards compatibility if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) { @@ -425,7 +425,7 @@ Http::init() if ( array_key_exists($namespace, $project->getAttribute('services', [])) && ! $project->getAttribute('services', [])[$namespace] - && ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -435,7 +435,7 @@ Http::init() if ( array_key_exists('rest', $project->getAttribute('apis', [])) && ! $project->getAttribute('apis', [])['rest'] - && ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -488,7 +488,7 @@ Http::init() $roles = $authorization->getRoles(); $shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' - && ! $user->isApp($roles) + && ! $user->isKey($roles) && ! $user->isPrivileged($roles) && $devKey->isEmpty(); @@ -611,7 +611,7 @@ Http::init() $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); if ($useCache) { $roles = $authorization->getRoles(); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles); diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index 4fda054fd6..dfb384f893 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -51,7 +51,7 @@ Http::init() } $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; diff --git a/app/realtime.php b/app/realtime.php index d8b70960b8..ce2dc41e54 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -858,7 +858,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true; if ( !$websocketEnabled - && !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 0cbaefa4b3..f33f591d52 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -122,9 +122,9 @@ class Key $secret = $key; } - $role = User::ROLE_APPS; + $role = User::ROLE_KEYS; $roles = Config::getParam('roles', []); - $scopes = $roles[User::ROLE_APPS]['scopes'] ?? []; + $scopes = $roles[User::ROLE_KEYS]['scopes'] ?? []; $expired = false; $guestKey = new Key( @@ -270,7 +270,7 @@ class Key $name = $key->getAttribute('name', 'UNKNOWN'); - $role = User::ROLE_APPS; + $role = User::ROLE_KEYS; $scopes = $key->getAttribute('scopes', []); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index e0464f7e52..9319cffc57 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -93,7 +93,7 @@ class Decrement extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index de090f9882..ba74545342 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -93,7 +93,7 @@ class Increment extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 2ade0b2b79..2e861dfcce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -201,7 +201,7 @@ class Create extends Action $documents = [$data]; } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index ecc5b152ec..e0863b8ac7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -107,7 +107,7 @@ class Delete extends Action ): void { $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index 06f0e9cf1c..c40e70d667 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -78,7 +78,7 @@ class Get extends Action public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index b86d934ffb..4a675615da 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -103,7 +103,7 @@ class Update extends Action $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index fb3d414097..ae940456f0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -108,7 +108,7 @@ class Upsert extends Action throw new Exception($this->getMissingPayloadException()); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index fdcbced6f3..afde3d0baa 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -88,7 +88,7 @@ class XList extends Action public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, ?Http $utopia = null): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index f06feccdee..8e085a9481 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -75,7 +75,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); // API keys and admins can read any transaction, regular users need permissions diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index fe2ad8dbae..de057ae15e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -120,7 +120,7 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $transaction = ($isAPIKey || $isPrivilegedUser) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 35264730f8..1089d815b5 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -161,7 +161,7 @@ class Create extends Base /* @var Document $function */ $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php index 0a9dd01b7e..9908669f84 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -67,7 +67,7 @@ class Get extends Base ) { $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php index 6ad2a5ae55..bf31fa0ced 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -77,7 +77,7 @@ class XList extends Base ) { $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index 8530475f0c..95ef2cbe28 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -115,7 +115,7 @@ class Create extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php index 6d8781d484..c16b374c78 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php @@ -84,7 +84,7 @@ class Delete extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php index c876004319..7e5d7d6879 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php @@ -90,7 +90,7 @@ class Get extends Action /* @type Document $bucket */ $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php index c9ce5796eb..d997fe3cc0 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php @@ -65,7 +65,7 @@ class Get extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index 68bc2cabae..0cca87c646 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -133,7 +133,7 @@ class Get extends Action $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php index 5b3fd02370..f069b1cc2a 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php @@ -90,7 +90,7 @@ class Get extends Action $disposition = $decoded['disposition'] ?? 'inline'; $dbForProject = $isInternal ? $dbForPlatform : $dbForProject; - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php index 407f3766df..fb1245d29c 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php @@ -81,7 +81,7 @@ class Update extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { @@ -110,7 +110,7 @@ class Update extends Action // Users can only manage their own roles, API keys and Admin users can manage any $roles = $authorization->getRoles(); - if (!$user->isApp($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) { + if (!$user->isKey($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php index b2f00da6d2..b78d582c47 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php @@ -91,7 +91,7 @@ class Get extends Action /* @type Document $bucket */ $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php index 945c4bfd7c..28dfa87c5d 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php @@ -80,7 +80,7 @@ class XList extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 5500a56cbc..3bffb091ba 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -103,7 +103,7 @@ class Create extends Action public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, array $platform, Password $proofForPassword, Token $proofForToken) { - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $invitee = new Document(); $hash = ''; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php index ef8d130855..556f6de52c 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php @@ -81,7 +81,7 @@ class Get extends Action $roles = $authorization->getRoles(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php index 540dc8a871..2198531e64 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php @@ -84,7 +84,7 @@ class Update extends Action } $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner'); if ($project->getId() === 'console') { diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php index 7835c8051f..45f7eb33aa 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php @@ -134,7 +134,7 @@ class XList extends Action $roles = $authorization->getRoles(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php index 0d20a58b6b..222b0968be 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php @@ -71,7 +71,7 @@ class Create extends Action public function action(string $teamId, string $name, array $roles, Response $response, User $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php index 934074d3c2..85247678f8 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php @@ -15,7 +15,7 @@ class Action extends UtopiaAction { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php index 211c6449dc..3004efe382 100644 --- a/src/Appwrite/Utopia/Database/Documents/User.php +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -17,7 +17,7 @@ class User extends Document public const ROLE_ADMIN = 'admin'; public const ROLE_DEVELOPER = 'developer'; public const ROLE_OWNER = 'owner'; - public const ROLE_APPS = 'apps'; + public const ROLE_KEYS = 'keys'; public const ROLE_SYSTEM = 'system'; public function getEmail(): ?string @@ -39,7 +39,7 @@ class User extends Document { $roles = []; - if (!$this->isApp($authorization->getRoles())) { + if (!$this->isKey($authorization->getRoles())) { if ($this->getId()) { $roles[] = Role::user($this->getId())->toString(); $roles[] = Role::users()->toString(); @@ -115,15 +115,15 @@ class User extends Document } /** - * Is App User? + * Is Key User? * * @param array $roles * * @return bool */ - public function isApp(array $roles): bool + public function isKey(array $roles): bool { - if (in_array(self::ROLE_APPS, $roles)) { + if (in_array(self::ROLE_KEYS, $roles)) { return true; } diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 24803eeaa7..1070d993c8 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -240,7 +240,7 @@ class Request extends UtopiaRequest $forwardedUserAgent = $this->getHeader('x-forwarded-user-agent'); if (!empty($forwardedUserAgent)) { $roles = $this->authorization->getRoles(); - $isAppUser = $this->user?->isApp($roles) ?? false; + $isAppUser = $this->user?->isKey($roles) ?? false; if ($isAppUser) { return $forwardedUserAgent; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 66e4a5f4ae..c8b573fc49 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -591,7 +591,7 @@ class Response extends SwooleResponse $roles = $this->authorization->getRoles(); $user = $this->user ?? new DBUser(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) { $data->setAttribute($key, ''); diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php index bcdb46180f..674fdf09aa 100644 --- a/tests/unit/Auth/KeyTest.php +++ b/tests/unit/Auth/KeyTest.php @@ -22,7 +22,7 @@ class KeyTest extends TestCase 'collections.read', 'documents.read', ]; - $roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes']; + $roleScopes = Config::getParam('roles', [])[User::ROLE_KEYS]['scopes']; $guestRoleScopes = Config::getParam('roles', [])[User::ROLE_GUESTS]['scopes']; $key = self::generateKey($projectId, $usage, $scopes); @@ -37,7 +37,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Ephemeral Key', $decoded->getName()); @@ -61,7 +61,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Ephemeral Key', $decoded->getName()); $this->assertEquals(['metric123'], $decoded->getDisabledMetrics()); @@ -123,7 +123,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -146,7 +146,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -194,7 +194,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -289,7 +289,7 @@ class KeyTest extends TestCase $this->assertEquals($teamId, $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals($scopes, $decoded->getScopes()); $this->assertEquals('Organization key', $decoded->getName()); @@ -336,7 +336,7 @@ class KeyTest extends TestCase $this->assertEquals($teamId, $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals($scopes, $decoded->getScopes()); $this->assertEquals('Organization key', $decoded->getName()); } diff --git a/tests/unit/Utopia/Database/Documents/UserTest.php b/tests/unit/Utopia/Database/Documents/UserTest.php index b3638e7d3a..5bd73db132 100644 --- a/tests/unit/Utopia/Database/Documents/UserTest.php +++ b/tests/unit/Utopia/Database/Documents/UserTest.php @@ -179,11 +179,11 @@ class UserTest extends TestCase $this->assertEquals(true, $user->isPrivileged([User::ROLE_ADMIN])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_DEVELOPER])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS])); $this->assertEquals(false, $user->isPrivileged([User::ROLE_SYSTEM])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, User::ROLE_APPS])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, Role::guests()->toString()])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS, User::ROLE_KEYS])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS, Role::guests()->toString()])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, Role::guests()->toString()])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); } @@ -192,19 +192,19 @@ class UserTest extends TestCase { $user = new User(); - $this->assertEquals(false, $user->isApp([])); - $this->assertEquals(false, $user->isApp([Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([Role::users()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_ADMIN])); - $this->assertEquals(false, $user->isApp([User::ROLE_DEVELOPER])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS])); - $this->assertEquals(false, $user->isApp([User::ROLE_SYSTEM])); + $this->assertEquals(false, $user->isKey([])); + $this->assertEquals(false, $user->isKey([Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([Role::users()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_ADMIN])); + $this->assertEquals(false, $user->isKey([User::ROLE_DEVELOPER])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS])); + $this->assertEquals(false, $user->isKey([User::ROLE_SYSTEM])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS, User::ROLE_APPS])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS, Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER, Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS, User::ROLE_KEYS])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS, Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER, Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); } public function testGuestRoles(): void @@ -327,7 +327,7 @@ class UserTest extends TestCase public function testAppUserRoles(): void { - $this->getAuthorization()->addRole(User::ROLE_APPS); + $this->getAuthorization()->addRole(User::ROLE_KEYS); $user = new User([ '$id' => ID::custom('123'), 'memberships' => [ From 39aeecc58cd48fe5195b40d28e888f1b1d033f2f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 18 May 2026 05:52:24 +0000 Subject: [PATCH 79/97] fix(audits): drop removed 'location' field from event payload (utopia-php/audit 2.4 schema) --- src/Appwrite/Platform/Workers/Audits.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 07d91ce009..09863b8626 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -100,7 +100,6 @@ class Audits extends Action 'resource' => $resource, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => '', 'data' => [ 'userId' => $actorUserId, 'userName' => $actorUserName, From 5f63100f4c639ef246b0a40793eafd5144422501 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 20 May 2026 05:16:37 +0000 Subject: [PATCH 80/97] fix(presences): rename isApp() callers to isKey() (new files from upstream 1.9.x merge) --- src/Appwrite/Platform/Modules/Presences/HTTP/Update.php | 2 +- src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php | 2 +- src/Appwrite/Presences/State.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php index 5387d3a91e..376359717b 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php @@ -128,7 +128,7 @@ class Update extends PlatformAction Event $queueForEvents ): void { $presenceState = new PresenceState(); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($userId && !$isAPIKey && !$isPrivilegedUser) { diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php index c85cb15f17..be658adbd1 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php @@ -128,7 +128,7 @@ class Upsert extends PlatformAction Event $queueForEvents, Context $usage ): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($userId && !$isAPIKey && !$isPrivilegedUser) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, "userId is not allowed for non-API key and non-privileged users"); diff --git a/src/Appwrite/Presences/State.php b/src/Appwrite/Presences/State.php index 19e7dc98b7..e7c51dec87 100644 --- a/src/Appwrite/Presences/State.php +++ b/src/Appwrite/Presences/State.php @@ -48,7 +48,7 @@ class State $permissions[] = (new Permission($permission, 'user', $ownerOverride))->toString(); } } else { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $permissions = Permission::aggregate($permissions, $allowedPermissions); From 3fc5fd8fcf5eaa048841faca1cf9242b20052d39 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 24 May 2026 03:08:25 +0000 Subject: [PATCH 81/97] chore: bump appwrite/php-runtimes to 0.20.1 --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 79b7db5389..db5b8fa09d 100644 --- a/composer.lock +++ b/composer.lock @@ -161,16 +161,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.20.0", + "version": "0.20.1", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3" + "reference": "e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7d9b7f4eef5c0a142a60907b06de2219d025c5c3", - "reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6", + "reference": "e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6", "shasum": "" }, "require": { @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.20.0" + "source": "https://github.com/appwrite/runtimes/tree/0.20.1" }, - "time": "2026-05-01T07:47:07+00:00" + "time": "2026-05-24T03:00:39+00:00" }, { "name": "brick/math", From d6ca57d0089d8a0d74972d5871e99ece2686d4c3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 May 2026 06:36:25 +0000 Subject: [PATCH 82/97] fix(uploads): reset queueForEvents on idempotent chunked-upload retry The early-return branches added in #12138 for already-complete chunked uploads bypass the trailing $queueForEvents->setParam('bucketId', ...) calls. The action returns cleanly, the shutdown hook then calls Event::generateEvents with empty params, throws \InvalidArgumentException, and the error handler maps it to HTTP 500. Reset the queue on each early-return path so the shutdown short-circuits on the empty event string -- the resource was created on a previous request and that request already fired the event; replaying it would double-trigger webhooks/functions. Applied to Storage Files Create, Functions Deployments Create, and Sites Deployments Create -- all three endpoints share the same pattern introduced by #12138. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Platform/Modules/Functions/Http/Deployments/Create.php | 3 +++ .../Platform/Modules/Sites/Http/Deployments/Create.php | 3 +++ .../Platform/Modules/Storage/Http/Buckets/Files/Create.php | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index 9af5491598..25863d424c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -227,6 +227,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -249,6 +250,8 @@ class Create extends Action $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); if ($uploaded === $chunks) { + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($deployment, Response::MODEL_DEPLOYMENT); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index d27755d106..5fd724346c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -227,6 +227,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -257,6 +258,8 @@ class Create extends Action $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); if ($uploaded === $chunks) { + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($deployment, Response::MODEL_DEPLOYMENT); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index 95ef2cbe28..348b19c039 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -320,6 +320,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -337,6 +338,8 @@ class Create extends Action throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); } + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic($file, Response::MODEL_FILE); From a659b0b1874554824043c34445ae532fe5966c27 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 25 May 2026 19:21:35 +0530 Subject: [PATCH 83/97] Use stable Utopia dependency releases --- composer.json | 4 +-- composer.lock | 95 +++++++++++++++++++++++---------------------------- 2 files changed, 45 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 65c5066ad1..73ea513792 100644 --- a/composer.json +++ b/composer.json @@ -76,12 +76,12 @@ "utopia-php/logger": "0.8.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.*", - "utopia-php/platform": "dev-feat/action-param-meta", + "utopia-php/platform": "1.0.0-rc3", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", "utopia-php/queue": "0.18.*", - "utopia-php/servers": "dev-feat-param-enum-metadata as 0.4.0", + "utopia-php/servers": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "2.*", "utopia-php/system": "0.10.*", diff --git a/composer.lock b/composer.lock index cee8d63fa2..fb34853980 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": "5e4279c99bb870892a68ecc99600e8bb", + "content-hash": "343aa35bfe6cd80cbe5d3150d00f8b85", "packages": [ { "name": "adhocore/jwt", @@ -3731,21 +3731,21 @@ }, { "name": "utopia-php/cli", - "version": "0.23.3", + "version": "0.23.4", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db" + "reference": "59f66e72ec1f8350968d8b95c0039faaac7a6d6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db", - "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/59f66e72ec1f8350968d8b95c0039faaac7a6d6d", + "reference": "59f66e72ec1f8350968d8b95c0039faaac7a6d6d", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/servers": "0.4.0" + "utopia-php/servers": "0.4.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -3776,9 +3776,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.23.3" + "source": "https://github.com/utopia-php/cli/tree/0.23.4" }, - "time": "2026-05-05T04:38:59+00:00" + "time": "2026-05-21T14:42:15+00:00" }, { "name": "utopia-php/compression", @@ -4346,23 +4346,23 @@ }, { "name": "utopia-php/http", - "version": "2.0.0-rc1", + "version": "2.0.0-rc3", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "3e3b431d443844c6bf810120dee735f45880856f" + "reference": "e1a51a2eb906a3d461378fb551a647ef01714f1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f", - "reference": "3e3b431d443844c6bf810120dee735f45880856f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e1a51a2eb906a3d461378fb551a647ef01714f1c", + "reference": "e1a51a2eb906a3d461378fb551a647ef01714f1c", "shasum": "" }, "require": { "php": ">=8.3", "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", - "utopia-php/servers": "0.4.0", + "utopia-php/servers": "0.4.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4396,9 +4396,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/2.0.0-rc1" + "source": "https://github.com/utopia-php/http/tree/2.0.0-rc3" }, - "time": "2026-05-05T15:00:03+00:00" + "time": "2026-05-21T14:42:41+00:00" }, { "name": "utopia-php/image", @@ -4773,16 +4773,16 @@ }, { "name": "utopia-php/platform", - "version": "dev-feat/action-param-meta", + "version": "1.0.0-rc3", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "c490c9aa38d8f0e04061b197fa99489450c2641a" + "reference": "674fd3cea6d1f8db660b01c5be89e8333dfb6de9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/c490c9aa38d8f0e04061b197fa99489450c2641a", - "reference": "c490c9aa38d8f0e04061b197fa99489450c2641a", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/674fd3cea6d1f8db660b01c5be89e8333dfb6de9", + "reference": "674fd3cea6d1f8db660b01c5be89e8333dfb6de9", "shasum": "" }, "require": { @@ -4792,7 +4792,7 @@ "utopia-php/cli": "0.23.*", "utopia-php/http": "^2.0@RC", "utopia-php/queue": "0.18.*", - "utopia-php/servers": "dev-feat-param-enum-metadata as 0.4.0" + "utopia-php/servers": "0.4.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -4818,9 +4818,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/feat/action-param-meta" + "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc3" }, - "time": "2026-05-20T12:10:54+00:00" + "time": "2026-05-25T11:48:18+00:00" }, { "name": "utopia-php/pools", @@ -4976,16 +4976,16 @@ }, { "name": "utopia-php/queue", - "version": "0.18.3", + "version": "0.18.4", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "141aad162b90728353f3aa834684b1f2affed045" + "reference": "1323b52f39b899c580dbb7af8562fc53d4e13e62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/141aad162b90728353f3aa834684b1f2affed045", - "reference": "141aad162b90728353f3aa834684b1f2affed045", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/1323b52f39b899c580dbb7af8562fc53d4e13e62", + "reference": "1323b52f39b899c580dbb7af8562fc53d4e13e62", "shasum": "" }, "require": { @@ -4993,7 +4993,7 @@ "php-amqplib/php-amqplib": "^3.7", "utopia-php/di": "0.3.*", "utopia-php/pools": "1.*", - "utopia-php/servers": "0.4.0", + "utopia-php/servers": "0.4.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -5036,9 +5036,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.18.3" + "source": "https://github.com/utopia-php/queue/tree/0.18.4" }, - "time": "2026-05-14T08:53:35+00:00" + "time": "2026-05-21T14:43:24+00:00" }, { "name": "utopia-php/registry", @@ -5094,16 +5094,16 @@ }, { "name": "utopia-php/servers", - "version": "dev-feat-param-enum-metadata", + "version": "0.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "333e93c5e5c629a1ae059003f570a48bc5a0eeb6" + "reference": "6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/333e93c5e5c629a1ae059003f570a48bc5a0eeb6", - "reference": "333e93c5e5c629a1ae059003f570a48bc5a0eeb6", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4", + "reference": "6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4", "shasum": "" }, "require": { @@ -5142,9 +5142,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/feat-param-enum-metadata" + "source": "https://github.com/utopia-php/servers/tree/0.4.1" }, - "time": "2026-05-20T11:51:30+00:00" + "time": "2026-05-21T13:08:45+00:00" }, { "name": "utopia-php/span", @@ -5355,16 +5355,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23" + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/b4ee60db4dbae5ffbe53968d01f69b6941251576", + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576", "shasum": "" }, "require": { @@ -5394,9 +5394,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.3" + "source": "https://github.com/utopia-php/validators/tree/0.2.4" }, - "time": "2026-05-14T08:05:44+00:00" + "time": "2026-05-21T12:47:43+00:00" }, { "name": "utopia-php/vcs", @@ -8615,19 +8615,10 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [ - { - "package": "utopia-php/servers", - "version": "dev-feat-param-enum-metadata", - "alias": "0.4.0", - "alias_normalized": "0.4.0.0" - } - ], + "aliases": [], "minimum-stability": "dev", "stability-flags": { - "utopia-php/http": 5, - "utopia-php/platform": 20, - "utopia-php/servers": 20 + "utopia-php/http": 5 }, "prefer-stable": true, "prefer-lowest": false, From ee97d4ef4da318a86eea5101d9545fe89458bda5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 25 May 2026 19:27:04 +0530 Subject: [PATCH 84/97] Use compatible HTTP release for platform --- composer.json | 2 +- composer.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index c9980581c8..5318db9f47 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "utopia-php/emails": "0.7.*", "utopia-php/dns": "1.7.*", "utopia-php/dsn": "0.2.1", - "utopia-php/http": "2.0.0-rc2", + "utopia-php/http": "2.0.0-rc3", "utopia-php/fetch": "^1.1", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", diff --git a/composer.lock b/composer.lock index 988b26b252..8f3ab9fd93 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": "af7afb20c293318eab8d80dff5fb2b01", + "content-hash": "0f60e85b461aa1a57d624e97be0614bd", "packages": [ { "name": "adhocore/jwt", @@ -4346,23 +4346,23 @@ }, { "name": "utopia-php/http", - "version": "2.0.0-rc2", + "version": "2.0.0-rc3", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3" + "reference": "e1a51a2eb906a3d461378fb551a647ef01714f1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/17f3d5e966ada8a5c041717436f069f269aef2b3", - "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e1a51a2eb906a3d461378fb551a647ef01714f1c", + "reference": "e1a51a2eb906a3d461378fb551a647ef01714f1c", "shasum": "" }, "require": { "php": ">=8.3", "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", - "utopia-php/servers": "0.4.0", + "utopia-php/servers": "0.4.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, @@ -4396,9 +4396,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/2.0.0-rc2" + "source": "https://github.com/utopia-php/http/tree/2.0.0-rc3" }, - "time": "2026-05-20T11:13:49+00:00" + "time": "2026-05-21T14:42:41+00:00" }, { "name": "utopia-php/image", @@ -5094,16 +5094,16 @@ }, { "name": "utopia-php/servers", - "version": "0.4.0", + "version": "0.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "7db346ef377503efe0acafe0791085270cd9ed70" + "reference": "6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70", - "reference": "7db346ef377503efe0acafe0791085270cd9ed70", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4", + "reference": "6e9241f587a19c0e9e7a3587fe3b15ffb2d4c2a4", "shasum": "" }, "require": { @@ -5142,9 +5142,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.4.0" + "source": "https://github.com/utopia-php/servers/tree/0.4.1" }, - "time": "2026-05-05T04:08:30+00:00" + "time": "2026-05-21T13:08:45+00:00" }, { "name": "utopia-php/span", From e19ca24d5dce74be187fbc3a0cfeccd77ba36128 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 25 May 2026 20:36:14 +0530 Subject: [PATCH 85/97] Update enum metadata tests --- .../Databases/Http/Databases/Usage/Get.php | 4 +- .../Databases/Http/Databases/Usage/XList.php | 4 +- tests/unit/SDK/Specification/FormatTest.php | 43 ++++++++++++------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php index 3060fe9978..39e57a0aee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php @@ -37,7 +37,9 @@ class Get extends Action default => DATABASE_TYPE_LEGACY, }; - return parent::setHttpPath($path); + parent::setHttpPath($path); + + return $this; } protected function getMetrics(): array diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php index 17b3ff94db..7dea8b9380 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php @@ -35,7 +35,9 @@ class XList extends Action default => DATABASE_TYPE_LEGACY, }; - return parent::setHttpPath($path); + parent::setHttpPath($path); + + return $this; } protected function getMetrics(): array diff --git a/tests/unit/SDK/Specification/FormatTest.php b/tests/unit/SDK/Specification/FormatTest.php index f99b29bfe2..ea9ed3bd43 100644 --- a/tests/unit/SDK/Specification/FormatTest.php +++ b/tests/unit/SDK/Specification/FormatTest.php @@ -3,6 +3,13 @@ namespace Tests\Unit\SDK\Specification; use Appwrite\SDK\Specification\Format; +use Appwrite\Utopia\Response\Model\HealthStatus; +use Appwrite\Utopia\Response\Model\PlatformAndroid; +use Appwrite\Utopia\Response\Model\PlatformApple; +use Appwrite\Utopia\Response\Model\PlatformLinux; +use Appwrite\Utopia\Response\Model\PlatformList; +use Appwrite\Utopia\Response\Model\PlatformWeb; +use Appwrite\Utopia\Response\Model\PlatformWindows; use PHPUnit\Framework\TestCase; use Utopia\DI\Container; @@ -18,9 +25,9 @@ class TestFormat extends Format return []; } - public function requestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array + public function requestParameterConfig(bool $optional, bool $nullable, mixed $default, string $methodName = '', string $paramName = ''): array { - return $this->getRequestParameterConfig($service, $method, $param, $optional, $nullable, $default); + return $this->getRequestParameterConfig($optional, $nullable, $default, $methodName, $paramName); } } @@ -37,9 +44,9 @@ class FormatTest extends TestCase public function testProjectRequestParameterOverrides(): void { - $createWebPlatform = $this->format->requestParameterConfig('project', 'createWebPlatform', 'hostname', true, false, ''); - $updateWebPlatform = $this->format->requestParameterConfig('project', 'updateWebPlatform', 'hostname', true, false, ''); - $listPlatforms = $this->format->requestParameterConfig('project', 'listPlatforms', 'queries', true, false, []); + $createWebPlatform = $this->format->requestParameterConfig(true, false, '', 'project.createWebPlatform', 'hostname'); + $updateWebPlatform = $this->format->requestParameterConfig(true, false, '', 'project.updateWebPlatform', 'hostname'); + $listPlatforms = $this->format->requestParameterConfig(true, false, [], 'project.listPlatforms', 'queries'); $this->assertTrue($createWebPlatform['required']); $this->assertFalse($createWebPlatform['emitDefault']); @@ -48,19 +55,25 @@ class FormatTest extends TestCase $this->assertTrue($listPlatforms['emitDefault']); } - public function testProjectPlatformResponseTypeUsesSharedEnumName(): void + public function testProjectPlatformResponseTypeUsesSharedEnumMetadata(): void { - $this->assertSame('PlatformType', $this->format->getResponseEnumName('platformAndroid', 'type')); - $this->assertSame('PlatformType', $this->format->getResponseEnumName('platformWeb', 'type')); - $this->assertSame('PlatformType', $this->format->getResponseEnumName('platformApple', 'type')); - $this->assertSame('PlatformType', $this->format->getResponseEnumName('platformWindows', 'type')); - $this->assertSame('PlatformType', $this->format->getResponseEnumName('platformLinux', 'type')); - $this->assertNull($this->format->getResponseEnumName('platformList', 'type')); + $models = [ + new PlatformAndroid(), + new PlatformWeb(), + new PlatformApple(), + new PlatformWindows(), + new PlatformLinux(), + ]; + + foreach ($models as $model) { + $this->assertSame('PlatformType', $model->getRules()['type']['enumSDKName']); + } + + $this->assertArrayNotHasKey('enumSDKName', (new PlatformList())->getRules()['platforms']); } - public function testExistingResponseEnumMappingsRemainUnchanged(): void + public function testExistingResponseEnumMetadataRemainsUnchanged(): void { - $this->assertSame('HealthCheckStatus', $this->format->getResponseEnumName('healthStatus', 'status')); - $this->assertNull($this->format->getResponseEnumName('key', 'name')); + $this->assertSame('HealthCheckStatus', (new HealthStatus())->getRules()['status']['enumSDKName']); } } From 88648b78f03d0514648ef256acf6b35d17779991 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:30:50 +0530 Subject: [PATCH 86/97] fix: preserve SDK enum metadata --- app/controllers/api/account.php | 5 +++-- app/controllers/api/messaging.php | 9 +++++---- app/controllers/api/project.php | 3 ++- app/controllers/api/users.php | 9 +++++---- .../Modules/Functions/Http/Executions/Create.php | 3 ++- .../Modules/Project/Http/Project/AuthMethods/Update.php | 3 ++- .../Platform/Modules/Project/Http/Project/OAuth2/Get.php | 3 ++- .../Project/Http/Project/OAuth2/Google/Update.php | 3 ++- .../Modules/Project/Http/Project/Policies/Get.php | 3 ++- .../Modules/Project/Http/Project/Protocols/Update.php | 3 ++- .../Modules/Project/Http/Project/SMTP/Update.php | 3 ++- .../Modules/Project/Http/Project/Services/Update.php | 3 ++- .../Modules/Project/Http/Project/Templates/Email/Get.php | 5 +++-- .../Project/Http/Project/Templates/Email/Update.php | 5 +++-- 14 files changed, 37 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c7da65f818..5c95738a69 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -67,6 +67,7 @@ use Utopia\Emails\Email; use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; +use Utopia\Platform\Enum; use Utopia\Storage\Validator\FileName; use Utopia\System\System; use Utopia\Validator; @@ -1242,7 +1243,7 @@ Http::get('/v1/account/sessions/oauth2/:provider') )) ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider')) ->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) @@ -2008,7 +2009,7 @@ Http::get('/v1/account/tokens/oauth2/:provider') )) ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider')) ->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index d1ffa2e478..26f6bf21c4 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -47,6 +47,7 @@ use Utopia\Database\Validator\UID; use Utopia\Emails\Validator\Email; use Utopia\Http\Http; use Utopia\Locale\Locale; +use Utopia\Platform\Enum; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; @@ -364,7 +365,7 @@ Http::post('/v1/messaging/providers/smtp') ->param('port', 587, new Range(1, 65535), 'The default SMTP server port.', true) ->param('username', '', new Text(0), 'Authentication username.', true) ->param('password', '', new Text(0), 'Authentication password.', true) - ->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true) + ->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption')) ->param('autoTLS', true, new Boolean(), 'Enable SMTP AutoTLS feature.', true) ->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true) ->param('fromName', '', new Text(128, 0), 'Sender Name.', true) @@ -1602,7 +1603,7 @@ Http::patch('/v1/messaging/providers/smtp/:providerId') ->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true) ->param('username', '', new Text(0), 'Authentication username.', true) ->param('password', '', new Text(0), 'Authentication password.', true) - ->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true) + ->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption')) ->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true) ->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true) ->param('fromName', '', new Text(128), 'Sender Name.', true) @@ -3501,7 +3502,7 @@ Http::post('/v1/messaging/messages/push') ->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true) ->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true) - ->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true) + ->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority')) ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') @@ -4388,7 +4389,7 @@ Http::patch('/v1/messaging/messages/push/:messageId') ->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true) ->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true) - ->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true) + ->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority')) ->inject('queueForEvents') ->inject('dbForProject') ->inject('dbForPlatform') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 544beade77..043227fad4 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -10,6 +10,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DateTimeValidator; use Utopia\Http\Http; +use Utopia\Platform\Enum; use Utopia\Validator\WhiteList; Http::get('/v1/project/usage') @@ -31,7 +32,7 @@ Http::get('/v1/project/usage') )) ->param('startDate', '', new DateTimeValidator(), 'Starting date for the usage') ->param('endDate', '', new DateTimeValidator(), 'End date for the usage') - ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true) + ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true, enum: new Enum(name: 'ProjectUsageRange')) ->inject('response') ->inject('project') ->inject('dbForProject') diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index abe06a2bb8..c8ea66ea51 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -63,6 +63,7 @@ use Utopia\Emails\Email; use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; +use Utopia\Platform\Enum; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; @@ -432,7 +433,7 @@ Http::post('/v1/users/sha') ->param('userId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password hashed using SHA.') - ->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true) + ->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true, enum: new Enum(name: 'PasswordHash')) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') ->inject('project') @@ -606,7 +607,7 @@ Http::post('/v1/users/:userId/targets') )) ->param('targetId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject']) - ->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') + ->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', enum: new Enum(name: 'MessagingProviderType')) ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') ->param('providerId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true, ['dbForProject']) ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true) @@ -2318,7 +2319,7 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type') ) ]) ->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject']) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType')) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -2831,7 +2832,7 @@ Http::get('/v1/users/usage') ) ] )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum(name: 'UsageRange')) ->inject('response') ->inject('dbForProject') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 1089d815b5..ba36b47884 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -38,6 +38,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\AnyOf; @@ -86,7 +87,7 @@ class Create extends Base ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) - ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], true), 'HTTP method of execution. Default value is POST.', true) + ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], true), 'HTTP method of execution. Default value is POST.', true, enum: new Enum(name: 'ExecutionMethod')) ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) ->param('scheduledAt', null, new Nullable(new Text(100)), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) ->inject('response') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php index 5335036cde..e98d3141f6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php @@ -12,6 +12,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\WhiteList; @@ -53,7 +54,7 @@ class Update extends Action ], )) - ->param('methodId', '', new WhiteList(\array_keys(Config::getParam('auth')), true), 'Auth Method ID. Possible values: ' . implode(',', \array_keys(Config::getParam('auth'))), false) + ->param('methodId', '', new WhiteList(\array_keys(Config::getParam('auth')), true), 'Auth Method ID. Possible values: ' . implode(',', \array_keys(Config::getParam('auth'))), false, enum: new Enum(name: 'ProjectAuthMethodId')) ->param('enabled', null, new Boolean(), 'Auth method status.') ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 250a3e5df1..57953e4e68 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -10,6 +10,7 @@ use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -86,7 +87,7 @@ class Get extends Action ) ] )) - ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider']) + ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'], enum: new Enum(name: 'ProjectOAuthProviderId')) ->inject('response') ->inject('project') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php index 2a061d09ce..5623f2bdf9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Platform\Enum; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Nullable; @@ -108,7 +109,7 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true) + ->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true, enum: new Enum(name: 'ProjectOAuth2GooglePrompt')) ->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true) ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php index 21342332d9..7505cf62c4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php @@ -8,6 +8,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\WhiteList; @@ -63,7 +64,7 @@ class Get extends Action 'session-limit', 'user-limit', 'membership-privacy', - ], true), 'Policy ID. Can be one of: password-dictionary, password-history, password-personal-data, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy.') + ], true), 'Policy ID. Can be one of: password-dictionary, password-history, password-personal-data, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy.', enum: new Enum(name: 'ProjectPolicyId')) ->inject('response') ->inject('project') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Protocols/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Protocols/Update.php index ad5691c1e0..bfc73802c8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Protocols/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Protocols/Update.php @@ -12,6 +12,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\WhiteList; @@ -53,7 +54,7 @@ class Update extends Action ) ], )) - ->param('protocolId', '', new WhiteList(array_keys(Config::getParam('protocols')), true), 'Protocol name. Can be one of: ' . \implode(', ', array_keys(Config::getParam('protocols')))) + ->param('protocolId', '', new WhiteList(array_keys(Config::getParam('protocols')), true), 'Protocol name. Can be one of: ' . \implode(', ', array_keys(Config::getParam('protocols'))), enum: new Enum(name: 'ProjectProtocolId')) ->param('enabled', null, new Boolean(), 'Protocol status.') ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php index b99a9db3c2..c3fe222298 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Emails\Validator\Email; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\Hostname; @@ -66,7 +67,7 @@ class Update extends Action ->param('senderName', null, new Nullable(new Text(256, 0)), 'Name shown in inbox as the sender of the email. Pass an empty string to clear a previously set value.', optional: true) ->param('replyToEmail', null, new Nullable(new Email(allowEmpty: true)), 'Email used when user replies to the email. Pass an empty string to clear a previously set value.', optional: true) ->param('replyToName', null, new Nullable(new Text(256, 0)), 'Name used when user replies to the email. Pass an empty string to clear a previously set value.', optional: true) - ->param('secure', null, new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true) + ->param('secure', null, new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true, enum: new Enum(name: 'ProjectSMTPSecure')) ->param('enabled', null, new Nullable(new Boolean()), 'Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates.', optional: true) ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Services/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Services/Update.php index 7aab6f5ad0..518eeb4c36 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Services/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Services/Update.php @@ -12,6 +12,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\WhiteList; @@ -53,7 +54,7 @@ class Update extends Action ) ], )) - ->param('serviceId', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name. Can be one of: '.\implode(', ', array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])))) + ->param('serviceId', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name. Can be one of: '.\implode(', ', array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']))), enum: new Enum(name: 'ProjectServiceId')) ->param('enabled', null, new Boolean(), 'Service status.') ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Get.php index 02ba431775..f5adfe9a6c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Get.php @@ -11,6 +11,7 @@ use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Locale\Locale; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\WhiteList; @@ -47,8 +48,8 @@ class Get extends Action ) ] )) - ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? [])) - ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes']) + ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []), enum: new Enum(name: 'ProjectEmailTemplateId')) + ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'], enum: new Enum(name: 'ProjectEmailTemplateLocale')) ->inject('response') ->inject('project') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php index c9c64ebdfa..c8ec04bc76 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php @@ -14,6 +14,7 @@ use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Emails\Validator\Email; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\Nullable; @@ -56,8 +57,8 @@ class Update extends Action ) ] )) - ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? [])) - ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes']) + ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []), enum: new Enum(name: 'ProjectEmailTemplateId')) + ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'], enum: new Enum(name: 'ProjectEmailTemplateLocale')) ->param('subject', null, new Nullable(new Text(255)), 'Subject of the email template. Can be up to 255 characters.', optional: true) ->param('message', null, new Nullable(new Text(10485760)), 'Plain or HTML body of the email template message. Can be up to 10MB of content.', optional: true) ->param('senderName', null, new Nullable(new Text(255, 0)), 'Name of the email sender.', optional: true) From 6266dfb7abbeae3a101f473a4fe343099fa72617 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:45:48 +0530 Subject: [PATCH 87/97] fix: preserve remaining SDK enum metadata --- app/controllers/api/account.php | 4 ++-- .../Platform/Modules/Avatars/Http/Screenshots/Get.php | 2 +- .../Platform/Modules/Console/Http/Templates/Email/Get.php | 5 +++-- .../Platform/Modules/Project/Http/Project/OAuth2/Get.php | 2 +- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 8 ++++++-- src/Appwrite/SDK/Specification/Format/Swagger2.php | 8 ++++++-- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5c95738a69..c8b1dfc98f 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1243,7 +1243,7 @@ Http::get('/v1/account/sessions/oauth2/:provider') )) ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider')) + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified'])) ->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) @@ -2009,7 +2009,7 @@ Http::get('/v1/account/tokens/oauth2/:provider') )) ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider')) + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified'])) ->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) ->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php index 69a4101711..26be113202 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php @@ -73,7 +73,7 @@ class Get extends Action ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') - ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York') + ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York', enum: new Enum()) ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749') ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') diff --git a/src/Appwrite/Platform/Modules/Console/Http/Templates/Email/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Templates/Email/Get.php index 6906c1fd79..003158ab9f 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Templates/Email/Get.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Templates/Email/Get.php @@ -11,6 +11,7 @@ use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Locale\Locale; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\WhiteList; @@ -46,8 +47,8 @@ class Get extends Action ) ] )) - ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? [])) - ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes']) + ->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? []), enum: new Enum(name: 'ProjectEmailTemplateId')) + ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'], enum: new Enum(name: 'ProjectEmailTemplateLocale')) ->inject('response') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 57953e4e68..58e8259fc5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -87,7 +87,7 @@ class Get extends Action ) ] )) - ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'], enum: new Enum(name: 'ProjectOAuthProviderId')) + ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'], enum: new Enum(name: 'ProjectOAuthProviderId', exclude: ['mock', 'mock-unverified'])) ->inject('response') ->inject('project') ->callback($this->action(...)); diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 20accca8ac..95e47cf2b7 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -655,7 +655,9 @@ class OpenAPI3 extends Format } $node['schema']['items']['enum'] = $enumValues; - $node['schema']['items']['x-enum-name'] = $enum->name; + if (!empty($enum->name)) { + $node['schema']['items']['x-enum-name'] = $enum->name; + } if (!empty($enumKeys)) { $node['schema']['items']['x-enum-keys'] = $enumKeys; } @@ -694,7 +696,9 @@ class OpenAPI3 extends Format } $node['schema']['enum'] = $enumValues; - $node['schema']['x-enum-name'] = $enum->name; + if (!empty($enum->name)) { + $node['schema']['x-enum-name'] = $enum->name; + } if (!empty($enumKeys)) { $node['schema']['x-enum-keys'] = $enumKeys; } diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 66244464a7..af2451e8ce 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -622,7 +622,9 @@ class Swagger2 extends Format } $node['items']['enum'] = $enumValues; - $node['items']['x-enum-name'] = $enum->name; + if (!empty($enum->name)) { + $node['items']['x-enum-name'] = $enum->name; + } if (!empty($enumKeys)) { $node['items']['x-enum-keys'] = $enumKeys; } @@ -661,7 +663,9 @@ class Swagger2 extends Format } $node['enum'] = $enumValues; - $node['x-enum-name'] = $enum->name; + if (!empty($enum->name)) { + $node['x-enum-name'] = $enum->name; + } if (!empty($enumKeys)) { $node['x-enum-keys'] = $enumKeys; } From 7f97d91f66d9dba4b4b876a601d04d147a9086b9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:48:13 +0530 Subject: [PATCH 88/97] fix: order spec defaults before examples --- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 7 ++++++- src/Appwrite/SDK/Specification/Format/Swagger2.php | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 95e47cf2b7..30a148e469 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -782,9 +782,14 @@ class OpenAPI3 extends Format $body['content'][$consumes[0]]['schema']['properties'][$name] = [ 'type' => $node['schema']['type'], 'description' => $node['description'], - 'x-example' => $node['schema']['x-example'] ?? null ]; + if (\array_key_exists('default', $node['schema'])) { + $body['content'][$consumes[0]]['schema']['properties'][$name]['default'] = $node['schema']['default']; + } + + $body['content'][$consumes[0]]['schema']['properties'][$name]['x-example'] = $node['schema']['x-example'] ?? null; + if (isset($node['schema']['format'])) { $body['content'][$consumes[0]]['schema']['properties'][$name]['format'] = $node['schema']['format']; } diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index af2451e8ce..524589c91e 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -757,13 +757,14 @@ class Swagger2 extends Format $body['schema']['properties'][$name] = [ 'type' => $node['type'], 'description' => $node['description'], - 'x-example' => $node['x-example'] ?? null, ]; if (\array_key_exists('default', $node)) { $body['schema']['properties'][$name]['default'] = $node['default']; } + $body['schema']['properties'][$name]['x-example'] = $node['x-example'] ?? null; + if (isset($node['format'])) { $body['schema']['properties'][$name]['format'] = $node['format']; } From 6bc5d0dd06558dc85b6e22fe221b341bfd58f8d4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:51:10 +0530 Subject: [PATCH 89/97] fix: remove endpoint docs appwrite metadata --- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 3 --- src/Appwrite/SDK/Specification/Format/Swagger2.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 30a148e469..2e7d0ea716 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -69,9 +69,6 @@ class OpenAPI3 extends Format ], ], ], - 'x-appwrite' => [ - 'endpointDocs' => $this->getParam('endpoint.docs', ''), - ], 'paths' => [], 'tags' => $this->services, 'components' => [ diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 524589c91e..c6353e67ba 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -56,9 +56,6 @@ class Swagger2 extends Format ], 'host' => \parse_url($this->getParam('endpoint', ''), PHP_URL_HOST), 'x-host-docs' => \parse_url($this->getParam('endpoint.docs', ''), PHP_URL_HOST), - 'x-appwrite' => [ - 'endpointDocs' => $this->getParam('endpoint.docs', ''), - ], 'basePath' => \parse_url($this->getParam('endpoint', ''), PHP_URL_PATH), 'schemes' => [\parse_url($this->getParam('endpoint', ''), PHP_URL_SCHEME)], 'consumes' => ['application/json', 'multipart/form-data'], From 17bae22b24159e0074fca1d2a1154a82bf3a3526 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:51:52 +0530 Subject: [PATCH 90/97] Revert "Merge pull request #12356 from appwrite/fix-account-server-specs-auth" This reverts commit ff76f3efa0ccba28f4ab3f13ae712a22014156d0, reversing changes made to ed14acfc75b62e85a4c0680e48a27f35808da7e9. --- app/config/sdks.php | 98 ++++++++++++++++----------------- app/controllers/api/account.php | 7 ++- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/app/config/sdks.php b/app/config/sdks.php index 36a973167d..e29b28690f 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -8,6 +8,55 @@ return [ 'enabled' => true, 'beta' => false, 'sdks' => [ + [ + 'key' => 'web', + 'name' => 'Web', + 'version' => '22.4.0', + 'url' => 'https://github.com/appwrite/sdk-for-web', + 'package' => 'https://www.npmjs.com/package/appwrite', + 'enabled' => true, + 'beta' => false, + 'dev' => false, + 'hidden' => false, + 'family' => APP_SDK_PLATFORM_CLIENT, + 'prism' => 'javascript', + 'source' => \realpath(__DIR__ . '/../sdks/client-web'), + 'gitUrl' => 'git@github.com:appwrite/sdk-for-web.git', + 'gitRepoName' => 'sdk-for-web', + 'gitUserName' => 'appwrite', + 'gitBranch' => 'dev', + 'changelog' => \realpath(__DIR__ . '/../../docs/sdks/web/CHANGELOG.md'), + 'demos' => [ + [ + 'icon' => 'react.svg', + 'name' => 'Todo App with React JS', + 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', + 'source' => 'https://github.com/appwrite/todo-with-react', + 'url' => 'https://appwrite-todo-with-react.vercel.app/', + ], + [ + 'icon' => 'vue.svg', + 'name' => 'Todo App with Vue JS', + 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', + 'source' => 'https://github.com/appwrite/todo-with-vue', + 'url' => 'https://appwrite-todo-with-vue.vercel.app/', + ], + [ + 'icon' => 'angular.svg', + 'name' => 'Todo App with Angular', + 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', + 'source' => 'https://github.com/appwrite/todo-with-angular', + 'url' => 'https://appwrite-todo-with-angular.vercel.app/', + ], + [ + 'icon' => 'svelte.svg', + 'name' => 'Todo App with Svelte', + 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', + 'source' => 'https://github.com/appwrite/todo-with-svelte', + 'url' => 'https://appwrite-todo-with-svelte.vercel.app/', + ], + ] + ], [ 'key' => 'flutter', 'name' => 'Flutter', @@ -301,55 +350,6 @@ return [ 'enabled' => true, 'beta' => false, 'sdks' => [ - [ - 'key' => 'web', - 'name' => 'Web', - 'version' => '22.4.0', - 'url' => 'https://github.com/appwrite/sdk-for-web', - 'package' => 'https://www.npmjs.com/package/appwrite', - 'enabled' => true, - 'beta' => false, - 'dev' => false, - 'hidden' => false, - 'family' => APP_SDK_PLATFORM_SERVER, - 'prism' => 'javascript', - 'source' => \realpath(__DIR__ . '/../sdks/client-web'), - 'gitUrl' => 'git@github.com:appwrite/sdk-for-web.git', - 'gitRepoName' => 'sdk-for-web', - 'gitUserName' => 'appwrite', - 'gitBranch' => 'dev', - 'changelog' => \realpath(__DIR__ . '/../../docs/sdks/web/CHANGELOG.md'), - 'demos' => [ - [ - 'icon' => 'react.svg', - 'name' => 'Todo App with React JS', - 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', - 'source' => 'https://github.com/appwrite/todo-with-react', - 'url' => 'https://appwrite-todo-with-react.vercel.app/', - ], - [ - 'icon' => 'vue.svg', - 'name' => 'Todo App with Vue JS', - 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', - 'source' => 'https://github.com/appwrite/todo-with-vue', - 'url' => 'https://appwrite-todo-with-vue.vercel.app/', - ], - [ - 'icon' => 'angular.svg', - 'name' => 'Todo App with Angular', - 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', - 'source' => 'https://github.com/appwrite/todo-with-angular', - 'url' => 'https://appwrite-todo-with-angular.vercel.app/', - ], - [ - 'icon' => 'svelte.svg', - 'name' => 'Todo App with Svelte', - 'description' => 'A simple Todo app that uses both the Appwrite account and database APIs.', - 'source' => 'https://github.com/appwrite/todo-with-svelte', - 'url' => 'https://appwrite-todo-with-svelte.vercel.app/', - ], - ] - ], [ 'key' => 'nodejs', 'name' => 'Node.js', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c8b1dfc98f..67affddd20 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1240,6 +1240,7 @@ Http::get('/v1/account/sessions/oauth2/:provider') ) ], contentType: ContentType::HTML, + hide: [APP_SDK_PLATFORM_SERVER], )) ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') @@ -4515,7 +4516,7 @@ Http::post('/v1/account/targets/push') group: 'pushTargets', name: 'createPushTarget', description: '/docs/references/account/create-push-target.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], + auth: [AuthType::ADMIN, AuthType::SESSION], responses: [ new SDKResponse( code: Response::STATUS_CODE_CREATED, @@ -4599,7 +4600,7 @@ Http::put('/v1/account/targets/:targetId/push') group: 'pushTargets', name: 'updatePushTarget', description: '/docs/references/account/update-push-target.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], + auth: [AuthType::ADMIN, AuthType::SESSION], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -4669,7 +4670,7 @@ Http::delete('/v1/account/targets/:targetId/push') group: 'pushTargets', name: 'deletePushTarget', description: '/docs/references/account/delete-push-target.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], + auth: [AuthType::ADMIN, AuthType::SESSION], responses: [ new SDKResponse( code: Response::STATUS_CODE_NOCONTENT, From 29d2a99e92562d5b29f148db5a80a51b3230963d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 10:53:50 +0530 Subject: [PATCH 91/97] fix: remove weight from spec metadata --- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 1 - src/Appwrite/SDK/Specification/Format/Swagger2.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 2e7d0ea716..b62397b2d0 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -147,7 +147,6 @@ class OpenAPI3 extends Format 'x-appwrite' => [ // Appwrite related metadata 'method' => $methodName, 'group' => $sdk->getGroup(), - 'weight' => $route->getOrder(), 'cookies' => $route->getLabel('sdk.cookies', false), 'type' => $sdk->getType()->value ?? '', 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodName) . '.md', diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index c6353e67ba..1232ea5408 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -143,7 +143,6 @@ class Swagger2 extends Format 'x-appwrite' => [ // Appwrite related metadata 'method' => $methodName, 'group' => $sdk->getGroup(), - 'weight' => $route->getOrder(), 'cookies' => $route->getLabel('sdk.cookies', false), 'type' => $sdk->getType()->value ?? '', 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodName) . '.md', From 6f1841681fbb942f527e295e658d5049a73783ed Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:02:52 +0530 Subject: [PATCH 92/97] fix: preserve runtime and migration enum metadata --- .../Platform/Modules/Functions/Http/Functions/Create.php | 2 +- .../Platform/Modules/Functions/Http/Functions/Update.php | 2 +- .../Modules/Migrations/Http/Migrations/Appwrite/Create.php | 2 +- .../Modules/Migrations/Http/Migrations/CSV/Imports/Create.php | 3 ++- .../Modules/Migrations/Http/Migrations/JSON/Imports/Create.php | 3 ++- src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php | 3 ++- src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php | 3 ++- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 3d5221a16d..f80866d79a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -81,7 +81,7 @@ class Create extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.') + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', enum: new Enum()) ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index ba72bafe6e..c1655a45bb 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -73,7 +73,7 @@ class Update extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true) + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true, enum: new Enum()) ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php index 34bc7ac4f2..329e5efeeb 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php @@ -59,7 +59,7 @@ class Create extends Action ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') ->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject']) ->param('apiKey', '', new Text(512), 'Source API Key') - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) ->inject('response') ->inject('dbForProject') ->inject('project') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php index 4b47ed7d58..4131948551 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php @@ -26,6 +26,7 @@ use Utopia\Migration\Sources\Appwrite as AppwriteSource; use Utopia\Migration\Sources\CSV; use Utopia\Migration\Transfer; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\System\System; @@ -69,7 +70,7 @@ class Create extends Action ->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File ID.', false, ['dbForProject']) ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.') ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true) - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php index c5d936711e..70900450a6 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php @@ -26,6 +26,7 @@ use Utopia\Migration\Sources\Appwrite as AppwriteSource; use Utopia\Migration\Sources\JSON as JSONSource; use Utopia\Migration\Transfer; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\System\System; @@ -68,7 +69,7 @@ class Create extends Action ->param('fileId', '', new UID(), 'File ID.') ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.') ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true) - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 9f3d46ffd0..717285266c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -17,6 +17,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -71,7 +72,7 @@ class Create extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.') + ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', enum: new Enum()) ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index bfd8c9f198..6951fafb48 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -20,6 +20,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; use Utopia\Validator\ArrayList; @@ -75,7 +76,7 @@ class Update extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true) + ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true, enum: new Enum()) ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) From 08b18f07749b76b01181ff0dc201b38c5cd539b3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:06:34 +0530 Subject: [PATCH 93/97] fix: name preserved enum metadata --- src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php | 2 +- .../Platform/Modules/Functions/Http/Functions/Create.php | 2 +- .../Platform/Modules/Functions/Http/Functions/Update.php | 2 +- .../Modules/Migrations/Http/Migrations/Appwrite/Create.php | 2 +- .../Modules/Migrations/Http/Migrations/CSV/Imports/Create.php | 2 +- .../Modules/Migrations/Http/Migrations/JSON/Imports/Create.php | 2 +- src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php | 2 +- src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php index 26be113202..7ff61f5b69 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php @@ -73,7 +73,7 @@ class Get extends Action ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') - ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York', enum: new Enum()) + ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York', enum: new Enum(name: 'Timezone')) ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749') ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index f80866d79a..e37f4120e1 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -81,7 +81,7 @@ class Create extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', enum: new Enum()) + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', enum: new Enum(name: 'FunctionRuntime')) ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index c1655a45bb..9272c2863c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -73,7 +73,7 @@ class Update extends Base )) ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Function name. Max length: 128 chars.') - ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true, enum: new Enum()) + ->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true, enum: new Enum(name: 'FunctionRuntime')) ->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php index 329e5efeeb..2633f5f6fb 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php @@ -59,7 +59,7 @@ class Create extends Action ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') ->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject']) ->param('apiKey', '', new Text(512), 'Source API Key') - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum(name: 'MigrationOnDuplicate')) ->inject('response') ->inject('dbForProject') ->inject('project') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php index 4131948551..7222a05703 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php @@ -70,7 +70,7 @@ class Create extends Action ->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File ID.', false, ['dbForProject']) ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.') ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true) - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum(name: 'MigrationOnDuplicate')) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php index 70900450a6..cec20fe61e 100644 --- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php +++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php @@ -69,7 +69,7 @@ class Create extends Action ->param('fileId', '', new UID(), 'File ID.') ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.') ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true) - ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum()) + ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true, enum: new Enum(name: 'MigrationOnDuplicate')) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 717285266c..e27854be92 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -72,7 +72,7 @@ class Create extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', enum: new Enum()) + ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', enum: new Enum(name: 'SiteBuildRuntime')) ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 6951fafb48..674a8280f6 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -76,7 +76,7 @@ class Update extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true, enum: new Enum()) + ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true, enum: new Enum(name: 'SiteBuildRuntime')) ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) From 3f0d1d2a2700cc3cfbf151e7289408bca503a18c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:14:52 +0530 Subject: [PATCH 94/97] fix: preserve remaining spec enum metadata --- .../Platform/Modules/Avatars/Http/Screenshots/Get.php | 2 +- .../Databases/Http/VectorsDB/Embeddings/Text/Create.php | 3 ++- .../Platform/Modules/Functions/Http/Templates/XList.php | 5 +++-- .../Platform/Modules/Health/Http/Health/Queue/Failed/Get.php | 3 ++- .../Platform/Modules/Projects/Http/Projects/Create.php | 3 ++- .../Platform/Modules/Projects/Http/Schedules/Create.php | 3 ++- .../Platform/Modules/Proxy/Http/Rules/Redirect/Create.php | 2 +- src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php | 4 ++-- src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php | 4 ++-- src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php | 5 +++-- .../Platform/Modules/Storage/Http/Buckets/Create.php | 3 ++- .../Platform/Modules/Storage/Http/Buckets/Update.php | 3 ++- 12 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php index 7ff61f5b69..9b2e92efe5 100644 --- a/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php +++ b/src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php @@ -69,7 +69,7 @@ class Get extends Action ->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920') ->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080') ->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2') - ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark') + ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark', enum: new Enum(name: 'BrowserTheme')) ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php index 8a7137e38b..b5480f39e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php @@ -16,6 +16,7 @@ use Utopia\Database\Document; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; use Utopia\Logger\Log; use Utopia\Logger\Logger; +use Utopia\Platform\Enum; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Text; @@ -74,7 +75,7 @@ class Create extends CreateDocumentAction ) ]) ->param('texts', [], fn (array $plan) => new ArrayList(new Text(0), $plan['databasesMaxEmbeddingTexts'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of text to generate embeddings.', false, ['plan']) - ->param('model', Ollama::MODEL_EMBEDDING_GEMMA, new WhiteList(Ollama::MODELS), 'The embedding model to use for generating vector embeddings.', true) + ->param('model', Ollama::MODEL_EMBEDDING_GEMMA, new WhiteList(Ollama::MODELS), 'The embedding model to use for generating vector embeddings.', true, enum: new Enum(name: 'EmbeddingModel')) ->inject('response') ->inject('project') ->inject('embeddingAgent') diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php index 91cb787b70..d77063e95a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php @@ -11,6 +11,7 @@ use FunctionUseCases; use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; @@ -50,8 +51,8 @@ class XList extends Base ) ] )) - ->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true) - ->param('useCases', [], new ArrayList(new WhiteList(FunctionUseCases::getAll()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true) + ->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true, enum: new Enum(name: 'FunctionRuntime')) + ->param('useCases', [], new ArrayList(new WhiteList(FunctionUseCases::getAll()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true, enum: new Enum(name: 'FunctionTemplateUseCase')) ->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true) ->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php index d3b760d01b..f0dff0a811 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php @@ -24,6 +24,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; +use Utopia\Platform\Enum; use Utopia\System\System; use Utopia\Validator\Integer; use Utopia\Validator\WhiteList; @@ -71,7 +72,7 @@ class Get extends Base System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME), System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME), System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME), - ]), 'The name of the queue') + ]), 'The name of the queue', enum: new Enum(name: 'HealthQueueName')) ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('response') ->inject('publisherForDatabase') diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index 18250cb140..9204f99d67 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -20,6 +20,7 @@ use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\DSN\DSN; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Pools\Group; use Utopia\System\System; @@ -54,7 +55,7 @@ class Create extends Action ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('teamId', '', new UID(), 'Team unique ID.') - ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true, enum: new Enum(name: 'Region')) ->inject('request') ->inject('response') ->inject('dbForPlatform') diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php index e00809300d..7cf13c5ca2 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Schedules/Create.php @@ -15,6 +15,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\JSON; @@ -85,7 +86,7 @@ class Create extends Action ], )) ->param('projectId', '', new UID(), 'Project unique ID.') - ->param('resourceType', '', new WhiteList($resourceTypes, true), 'The resource type for the schedule. Possible values: '.implode(', ', $resourceTypes).'.') + ->param('resourceType', '', new WhiteList($resourceTypes, true), 'The resource type for the schedule. Possible values: '.implode(', ', $resourceTypes).'.', enum: new Enum(name: 'ScheduleResourceType')) ->param('resourceId', '', new UID(), 'The resource ID to associate with this schedule.') ->param('schedule', '', new Cron(), 'Schedule CRON expression.') ->param('active', false, new Boolean(), 'Whether the schedule is active.', true) diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index b6f5185422..784da4e2eb 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -67,7 +67,7 @@ class Create extends Action ->label('abuse-time', 60) ->param('domain', null, new ValidatorDomain(), 'Domain name.') ->param('url', null, new URL(), 'Target URL of redirection') - ->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection') + ->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection', enum: new Enum(name: 'RedirectStatusCode')) ->param('resourceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'ID of parent resource.', false, ['dbForProject']) ->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.', enum: new Enum( name: 'ProxyResourceType', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index e27854be92..064f2e213e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -64,7 +64,7 @@ class Create extends Base )) ->param('siteId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Site name. Max length: 128 chars.') - ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.') + ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.', enum: new Enum(name: 'SiteFramework')) ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true) ->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true) @@ -73,7 +73,7 @@ class Create extends Base ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', enum: new Enum(name: 'SiteBuildRuntime')) - ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true, enum: new Enum(name: 'SiteAdapter')) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 674a8280f6..66537edb04 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -68,7 +68,7 @@ class Update extends Base )) ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject']) ->param('name', '', new Text(128), 'Site name. Max length: 128 chars.') - ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.') + ->param('framework', '', new WhiteList(\array_keys(Config::getParam('frameworks')), true), 'Sites framework.', enum: new Enum(name: 'SiteFramework')) ->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) ->param('logging', true, new Boolean(), 'When disabled, request logs will exclude logs and errors, and site responses will be slightly faster.', true) ->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true) @@ -77,7 +77,7 @@ class Update extends Base ->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true, enum: new Enum(name: 'SiteBuildRuntime')) - ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true, enum: new Enum(name: 'SiteAdapter')) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php index 4dea0908cf..a5c6ece560 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php @@ -11,6 +11,7 @@ use SiteUseCases; use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\ArrayList; use Utopia\Validator\Range; @@ -49,8 +50,8 @@ class XList extends Base ) ] )) - ->param('frameworks', [], new ArrayList(new WhiteList(\array_keys(Config::getParam('frameworks')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of frameworks allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' frameworks are allowed.', true) - ->param('useCases', [], new ArrayList(new WhiteList(SiteUseCases::getAll()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true) + ->param('frameworks', [], new ArrayList(new WhiteList(\array_keys(Config::getParam('frameworks')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of frameworks allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' frameworks are allowed.', true, enum: new Enum(name: 'SiteFramework')) + ->param('useCases', [], new ArrayList(new WhiteList(SiteUseCases::getAll()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true, enum: new Enum(name: 'SiteTemplateUseCase')) ->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true) ->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true) ->inject('response') diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php index f31d30188a..ebe06d0ccf 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Create.php @@ -17,6 +17,7 @@ use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Storage; use Utopia\System\System; @@ -68,7 +69,7 @@ class Create extends Action ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm chosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm chosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true, enum: new Enum(name: 'Compression')) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php index 406f8e4f49..3549c035f1 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php @@ -14,6 +14,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Storage; use Utopia\System\System; @@ -65,7 +66,7 @@ class Update extends Action ->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true) ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan']) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm chosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm chosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true, enum: new Enum(name: 'Compression')) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true) From fe1a2f6364b9e739d0477a0f2b295b196baf4f47 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:19:48 +0530 Subject: [PATCH 95/97] fix: preserve organization project region enum --- .../Platform/Modules/Organization/Http/Projects/Create.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php index 227c64ce8c..785fddf233 100644 --- a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Create.php @@ -21,6 +21,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\DSN\DSN; +use Utopia\Platform\Enum; use Utopia\Platform\Scope\HTTP; use Utopia\Pools\Group; use Utopia\System\System; @@ -64,7 +65,7 @@ class Create extends Action )) ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') - ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true, enum: new Enum(name: 'Region')) ->inject('response') ->inject('dbForPlatform') ->inject('cache') From a4d889145b959588b2c655c8c0a5d30e1d00ea01 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:28:30 +0530 Subject: [PATCH 96/97] test: expect client web version --- tests/e2e/General/HTTPTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 0358281eb7..450e4f2378 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -101,7 +101,7 @@ class HTTPTest extends Scope $body = $response['body']; $this->assertEquals(200, $response['headers']['status-code']); $this->assertIsString($body['server']); - $this->assertIsString($body['server-web']); + $this->assertIsString($body['client-web']); $this->assertIsString($body['client-flutter']); $this->assertIsString($body['console-web']); $this->assertIsString($body['server-nodejs']); From c7caf8d23d52cda9ccf16848773bf2bae1f51dc5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 26 May 2026 11:33:25 +0530 Subject: [PATCH 97/97] fix: map numeric spec enum keys --- app/controllers/api/project.php | 8 +++++++- .../Modules/Proxy/Http/Rules/Redirect/Create.php | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 043227fad4..b83c0a38ed 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -32,7 +32,13 @@ Http::get('/v1/project/usage') )) ->param('startDate', '', new DateTimeValidator(), 'Starting date for the usage') ->param('endDate', '', new DateTimeValidator(), 'End date for the usage') - ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true, enum: new Enum(name: 'ProjectUsageRange')) + ->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true, enum: new Enum( + name: 'ProjectUsageRange', + map: [ + '1h' => 'OneHour', + '1d' => 'OneDay', + ] + )) ->inject('response') ->inject('project') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index 784da4e2eb..8dd15d3f03 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -67,7 +67,15 @@ class Create extends Action ->label('abuse-time', 60) ->param('domain', null, new ValidatorDomain(), 'Domain name.') ->param('url', null, new URL(), 'Target URL of redirection') - ->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection', enum: new Enum(name: 'RedirectStatusCode')) + ->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection', enum: new Enum( + name: 'RedirectStatusCode', + map: [ + '301' => 'MovedPermanently', + '302' => 'Found', + '307' => 'TemporaryRedirect', + '308' => 'PermanentRedirect', + ] + )) ->param('resourceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'ID of parent resource.', false, ['dbForProject']) ->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.', enum: new Enum( name: 'ProxyResourceType',