From 80ad740d36f9529cf9653baacae4ec0f6bae1caf Mon Sep 17 00:00:00 2001 From: Tejas Raskar Date: Wed, 20 Aug 2025 14:34:43 +0530 Subject: [PATCH 01/16] docs: update the directory structure in CONTRIBUTING.md --- CONTRIBUTING.md | 78 +++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6837673d5..96b0614165 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,51 +222,73 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi ```bash . ├── app # Main application +│ ├── assets +│ │ ├── dbip +│ │ ├── fonts +│ │ └── security │ ├── config # Config files +│ │ ├── avatars +│ │ ├── collections +│ │ ├── locale +│ │ ├── specs +│ │ ├── storage +│ │ └── templates │ ├── controllers # API & dashboard controllers │ │ ├── api │ │ ├── shared │ │ └── web -│ ├── db # DB schemas -│ ├── sdks # SDKs generated copies (used for generating code examples) -│ ├── tasks # Server CLI commands -│ ├── views # HTML server-side templates -│ └── workers # Background workers +│ ├── init # DB schemas +│ │ └── database +│ └── views # HTML server-side templates +│ ├── general +│ └── install ├── bin # Server executables (tasks & workers) -├── docker # Docker related resources and configs +├── dev # Debugger config ├── docs # Docs and tutorials │ ├── examples +│ ├── lists │ ├── references +│ ├── sdks │ ├── services │ ├── specs │ └── tutorials ├── public # Public files -│ ├── dist │ ├── fonts │ ├── images -│ ├── scripts -│ └── styles -├── src # Supporting libraries (each lib has one role, common libs are released as individual projects) -│ └── Appwrite -│ ├── Auth -│ ├── Detector -│ ├── Docker -| ├── DSN -│ ├── Event -│ ├── Extend -│ ├── GraphQL -│ ├── Messaging -│ ├── Migration -│ ├── Network -│ ├── OpenSSL -│ ├── Promises -│ ├── Specification -│ ├── Task -│ ├── Template -│ ├── URL -│ └── Utopia +│ ├── sdk-console +│ ├── sdk-project +│ └── sdk-web +├── src # Supporting libraries (each lib has one role, common libs are released as +│ ├── Appwrite +│ │ ├── Auth +│ │ ├── Certificates +│ │ ├── Deletes +│ │ ├── Detector +│ │ ├── Docker +│ │ ├── Event +│ │ ├── Extend +│ │ ├── Functions/Validator +│ │ ├── GraphQL +│ │ ├── Hooks +│ │ ├── Messaging +│ │ ├── Migration +│ │ ├── Network +│ │ ├── OpenSSL +│ │ ├── Platform +│ │ ├── Promises +│ │ ├── PubSub +│ │ ├── SDK +│ │ ├── Task/Validator +│ │ ├── Template +│ │ ├── Transformation +│ │ ├── URL +│ │ ├── Utopia +│ │ └── Vcs +│ └── Executor └── tests # End to end & unit tests + ├── benchmarks ├── e2e + ├── extensions ├── resources └── unit ``` From aed9816d1e8d2d92ca27d2717934f4195395e3ad Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 26 Jan 2026 12:53:40 +0000 Subject: [PATCH 02/16] fix: validate relationship document ID --- .../Collections/Documents/Action.php | 31 +++ .../Collections/Documents/Create.php | 10 +- .../Collections/Documents/Update.php | 11 +- .../Collections/Documents/Upsert.php | 11 +- .../Databases/TablesDB/DatabasesBase.php | 251 ++++++++++++++++++ 5 files changed, 288 insertions(+), 26 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 14b09777a8..03236471db 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction; +use Appwrite\Utopia\Database\Validator\CustomId; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; abstract class Action extends DatabasesAction @@ -249,6 +251,35 @@ abstract class Action extends DatabasesAction return $document; } + /** + * Validate and normalize a relationship value. + * Returns the relation ID and normalized relation as an array. + */ + protected function validateRelationship(mixed $relation): array + { + $relationId = null; + + if ($relation instanceof Document) { + $relationId = $relation->getAttribute('$id'); + } elseif (\is_string($relation)) { + $relationId = $relation; + } elseif (\is_array($relation) && \array_values($relation) !== $relation) { + $relation['$id'] = ID::unique(); + $relation = new Document($relation); + } else { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object or document ID string, not ' . \gettype($relation)); + } + + if ($relationId !== null) { + $validator = new CustomId(); + if (!$validator->isValid($relationId)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); + } + } + + return [$relationId, $relation]; + } + /** * Resolves relationships in a document and attaches metadata. */ 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 d871abae8e..5244efc2ab 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 @@ -309,14 +309,8 @@ class Create extends Action ); foreach ($relations as &$relation) { - if ( - \is_array($relation) - && \array_values($relation) !== $relation - && !isset($relation['$id']) - ) { - $relation['$id'] = ID::unique(); - $relation = new Document($relation); - } + [$relationId, $relation] = $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); 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 a92d8ec180..f6fa6a95cc 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 @@ -201,15 +201,8 @@ class Update extends Action ); foreach ($relations as &$relation) { - // If the relation is an array it can be either update or create a child document. - if ( - \is_array($relation) - && \array_values($relation) !== $relation - && !isset($relation['$id']) - ) { - $relation['$id'] = ID::unique(); - $relation = new Document($relation); - } + [$relationId, $relation] = $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $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 62e59dd010..9cc38050c4 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 @@ -211,15 +211,8 @@ class Upsert extends Action ); foreach ($relations as &$relation) { - // If the relation is an array it can be either update or create a child document. - if ( - \is_array($relation) - && \array_values($relation) !== $relation - && !isset($relation['$id']) - ) { - $relation['$id'] = ID::unique(); - $relation = new Document($relation); - } + [$relationId, $relation] = $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index ba111e5923..4d0e9e76a2 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7626,6 +7626,257 @@ trait DatabasesBase $this->assertEquals(200, $update['headers']['status-code']); } + /** + * @depends testCreateDatabase + */ + public function testInvalidRelationshipDocumentId(array $data): void + { + $databaseId = $data['databaseId']; + + // Create parent table + $parentTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'ParentTable', + ]); + $this->assertEquals(201, $parentTable['headers']['status-code']); + $parentTableId = $parentTable['body']['$id']; + + // Create child table + $childTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'ChildTable', + ]); + $this->assertEquals(201, $childTable['headers']['status-code']); + $childTableId = $childTable['body']['$id']; + + // Add string column to parent + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 255, + 'required' => false, + ]); + + // Add string column to child + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $childTableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => false, + ]); + + // Create one-to-many relationship + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedTableId' => $childTableId, + 'type' => Database::RELATION_ONE_TO_MANY, + 'twoWay' => false, + 'key' => 'children', + ]); + + sleep(1); + + // ID too long (>36 chars) should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 1', + 'children' => [ + [ + '$id' => 'this_id_is_way_too_long_and_should_fail_validation_check', + 'title' => 'Child 1', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // ID with invalid characters should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 2', + 'children' => [ + [ + '$id' => 'invalid@id#with$special%chars', + 'title' => 'Child 2', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // ID starting with underscore should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 3', + 'children' => [ + [ + '$id' => '_startsWithUnderscore', + 'title' => 'Child 3', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Valid ID should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 4', + 'children' => [ + [ + '$id' => 'valid-id-123', + 'title' => 'Child 4', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + $parentRowId = $response['body']['$id']; + + // Update with invalid relationship ID should fail + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows/' . $parentRowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'children' => [ + [ + '$id' => 'another@invalid#id', + 'title' => 'Child 5', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Invalid string relation ID should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 6', + 'children' => [ + 'invalid@string#id', + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Integer as relation value should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 7', + 'children' => [ + 12345, + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // unique() as $id should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 8', + 'children' => [ + [ + '$id' => 'unique()', + 'title' => 'Child 8', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + // Empty string as $id should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 9', + 'children' => [ + [ + '$id' => '', + 'title' => 'Child 9', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Valid ID with allowed special chars (hyphen, period) should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 10', + 'children' => [ + [ + '$id' => 'valid.id-with_chars', + 'title' => 'Child 10', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + } + /** * @depends testCreateDatabase */ From f66e0c2ff57b7dbae8d473d8b6b638abf489926f Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 26 Jan 2026 14:05:56 +0000 Subject: [PATCH 03/16] refactor: separate validation from normalization in validateRelationship --- .../Http/Databases/Collections/Documents/Action.php | 13 +++---------- .../Http/Databases/Collections/Documents/Create.php | 7 ++++++- .../Http/Databases/Collections/Documents/Update.php | 7 ++++++- .../Http/Databases/Collections/Documents/Upsert.php | 7 ++++++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 03236471db..2a34c8979b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -8,7 +8,6 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction use Appwrite\Utopia\Database\Validator\CustomId; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; abstract class Action extends DatabasesAction @@ -252,10 +251,9 @@ abstract class Action extends DatabasesAction } /** - * Validate and normalize a relationship value. - * Returns the relation ID and normalized relation as an array. + * Validate a relationship value and its document ID. */ - protected function validateRelationship(mixed $relation): array + protected function validateRelationship(mixed $relation): void { $relationId = null; @@ -263,10 +261,7 @@ abstract class Action extends DatabasesAction $relationId = $relation->getAttribute('$id'); } elseif (\is_string($relation)) { $relationId = $relation; - } elseif (\is_array($relation) && \array_values($relation) !== $relation) { - $relation['$id'] = ID::unique(); - $relation = new Document($relation); - } else { + } elseif (!(\is_array($relation) && \array_values($relation) !== $relation)) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object or document ID string, not ' . \gettype($relation)); } @@ -276,8 +271,6 @@ abstract class Action extends DatabasesAction throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); } } - - return [$relationId, $relation]; } /** 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 5244efc2ab..8927c6b27b 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 @@ -309,7 +309,12 @@ class Create extends Action ); foreach ($relations as &$relation) { - [$relationId, $relation] = $this->validateRelationship($relation); + $this->validateRelationship($relation); + + if (\is_array($relation) && \array_values($relation) !== $relation) { + $relation['$id'] = ID::unique(); + $relation = new Document($relation); + } if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); 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 f6fa6a95cc..0f3eb9e026 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 @@ -201,7 +201,12 @@ class Update extends Action ); foreach ($relations as &$relation) { - [$relationId, $relation] = $this->validateRelationship($relation); + $this->validateRelationship($relation); + + if (\is_array($relation) && \array_values($relation) !== $relation) { + $relation['$id'] = ID::unique(); + $relation = new Document($relation); + } if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $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 9cc38050c4..cdff15c5ab 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 @@ -211,7 +211,12 @@ class Upsert extends Action ); foreach ($relations as &$relation) { - [$relationId, $relation] = $this->validateRelationship($relation); + $this->validateRelationship($relation); + + if (\is_array($relation) && \array_values($relation) !== $relation) { + $relation['$id'] = ID::unique(); + $relation = new Document($relation); + } if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); From 1ee2539ce0d8a247341aa0ca6481b63d98dad1fe Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 26 Jan 2026 14:49:43 +0000 Subject: [PATCH 04/16] fix: generate unique ID before validation per coderabbit suggestion --- .../Databases/Collections/Documents/Action.php | 4 +++- .../Databases/Collections/Documents/Create.php | 11 ++++++++++- .../Databases/Collections/Documents/Update.php | 11 ++++++++++- .../Databases/Collections/Documents/Upsert.php | 11 ++++++++++- .../Databases/TablesDB/DatabasesBase.php | 18 ++++++++++++++++-- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 2a34c8979b..efd3f4ed6f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -261,7 +261,9 @@ abstract class Action extends DatabasesAction $relationId = $relation->getAttribute('$id'); } elseif (\is_string($relation)) { $relationId = $relation; - } elseif (!(\is_array($relation) && \array_values($relation) !== $relation)) { + } elseif (\is_array($relation) && \array_values($relation) !== $relation) { + $relationId = $relation['$id'] ?? null; + } else { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object or document ID string, not ' . \gettype($relation)); } 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 8927c6b27b..253cf8ec3c 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 @@ -309,10 +309,19 @@ class Create extends Action ); foreach ($relations as &$relation) { + // Generate unique ID for new relation without $id + if ( + \is_array($relation) + && \array_values($relation) !== $relation + && !isset($relation['$id']) + ) { + $relation['$id'] = ID::unique(); + } + $this->validateRelationship($relation); + // If the relation is an array it can be either update or create a child document. if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation['$id'] = ID::unique(); $relation = new Document($relation); } 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 0f3eb9e026..34f2a45e15 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 @@ -201,10 +201,19 @@ class Update extends Action ); foreach ($relations as &$relation) { + // Generate unique ID for new relation without $id + if ( + \is_array($relation) + && \array_values($relation) !== $relation + && !isset($relation['$id']) + ) { + $relation['$id'] = ID::unique(); + } + $this->validateRelationship($relation); + // If the relation is an array it can be either update or create a child document. if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation['$id'] = ID::unique(); $relation = new Document($relation); } 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 cdff15c5ab..8b500a9e61 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 @@ -211,10 +211,19 @@ class Upsert extends Action ); foreach ($relations as &$relation) { + // Generate unique ID for new relation without $id + if ( + \is_array($relation) + && \array_values($relation) !== $relation + && !isset($relation['$id']) + ) { + $relation['$id'] = ID::unique(); + } + $this->validateRelationship($relation); + // If the relation is an array it can be either update or create a child document. if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation['$id'] = ID::unique(); $relation = new Document($relation); } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index 4d0e9e76a2..2ea0c8c108 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7680,7 +7680,7 @@ trait DatabasesBase ]); // Create one-to-many relationship - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/relationship', array_merge([ + $relationship = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/relationship', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -7690,8 +7690,22 @@ trait DatabasesBase 'twoWay' => false, 'key' => 'children', ]); + $this->assertEquals(202, $relationship['headers']['status-code']); - sleep(1); + // Wait for relationship column to be available + $maxAttempts = 10; + for ($i = 0; $i < $maxAttempts; $i++) { + $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $columnKeys = array_column($columns['body']['columns'], 'key'); + if (in_array('children', $columnKeys)) { + break; + } + usleep(200000); + } // ID too long (>36 chars) should fail $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ From 63e6a51af1185f27116909619ec1b08db4935fce Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 26 Jan 2026 15:31:41 +0000 Subject: [PATCH 05/16] test: add assertion for relationship column polling --- tests/e2e/Services/Databases/TablesDB/DatabasesBase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index 2ea0c8c108..8d8133241b 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7694,6 +7694,7 @@ trait DatabasesBase // Wait for relationship column to be available $maxAttempts = 10; + $childrenFound = false; for ($i = 0; $i < $maxAttempts; $i++) { $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns', array_merge([ 'content-type' => 'application/json', @@ -7702,10 +7703,12 @@ trait DatabasesBase ])); $columnKeys = array_column($columns['body']['columns'], 'key'); if (in_array('children', $columnKeys)) { + $childrenFound = true; break; } usleep(200000); } + $this->assertTrue($childrenFound, "Relationship column 'children' not found in table {$parentTableId} of database {$databaseId}"); // ID too long (>36 chars) should fail $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ From 00d091513d7d748a50ac77bd5e8b065a51f24ffb Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 27 Jan 2026 06:59:53 +0000 Subject: [PATCH 06/16] refactor: simplify relationship validation code --- .../Http/Databases/Collections/Documents/Action.php | 5 ++--- .../Http/Databases/Collections/Documents/Create.php | 7 +------ .../Http/Databases/Collections/Documents/Update.php | 8 ++------ .../Http/Databases/Collections/Documents/Upsert.php | 8 ++------ 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index efd3f4ed6f..c43c6114ef 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -251,7 +251,8 @@ abstract class Action extends DatabasesAction } /** - * Validate a relationship value and its document ID. + * Validate relationship values. + * Handles Document objects, ID strings, and associative arrays. */ protected function validateRelationship(mixed $relation): void { @@ -263,8 +264,6 @@ abstract class Action extends DatabasesAction $relationId = $relation; } elseif (\is_array($relation) && \array_values($relation) !== $relation) { $relationId = $relation['$id'] ?? null; - } else { - throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object or document ID string, not ' . \gettype($relation)); } if ($relationId !== null) { 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 253cf8ec3c..eebe59796e 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 @@ -309,22 +309,17 @@ class Create extends Action ); foreach ($relations as &$relation) { - // Generate unique ID for new relation without $id if ( \is_array($relation) && \array_values($relation) !== $relation && !isset($relation['$id']) ) { $relation['$id'] = ID::unique(); + $relation = new Document($relation); } $this->validateRelationship($relation); - // If the relation is an array it can be either update or create a child document. - if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation = new Document($relation); - } - if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); 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 34f2a45e15..ff3ab6e23c 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 @@ -201,22 +201,18 @@ class Update extends Action ); foreach ($relations as &$relation) { - // Generate unique ID for new relation without $id + // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) && \array_values($relation) !== $relation && !isset($relation['$id']) ) { $relation['$id'] = ID::unique(); + $relation = new Document($relation); } $this->validateRelationship($relation); - // If the relation is an array it can be either update or create a child document. - if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation = new Document($relation); - } - if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $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 8b500a9e61..d0536b65ef 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 @@ -211,22 +211,18 @@ class Upsert extends Action ); foreach ($relations as &$relation) { - // Generate unique ID for new relation without $id + // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) && \array_values($relation) !== $relation && !isset($relation['$id']) ) { $relation['$id'] = ID::unique(); + $relation = new Document($relation); } $this->validateRelationship($relation); - // If the relation is an array it can be either update or create a child document. - if (\is_array($relation) && \array_values($relation) !== $relation) { - $relation = new Document($relation); - } - if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); From d792d3bbeaae56a29e94e2191e0188ffb2ac2224 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 27 Jan 2026 09:25:39 +0000 Subject: [PATCH 07/16] refactor: use getId() instead of getAttribute('$id') --- .../Databases/Http/Databases/Collections/Documents/Action.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index c43c6114ef..65b3be2130 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -259,7 +259,7 @@ abstract class Action extends DatabasesAction $relationId = null; if ($relation instanceof Document) { - $relationId = $relation->getAttribute('$id'); + $relationId = $relation->getId(); } elseif (\is_string($relation)) { $relationId = $relation; } elseif (\is_array($relation) && \array_values($relation) !== $relation) { From d182c853302e6e717eb7d291e6b1d4dd88bb9b1d Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 27 Jan 2026 09:35:45 +0000 Subject: [PATCH 08/16] fix: reject unsupported relationship value types --- .../Databases/Http/Databases/Collections/Documents/Action.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 65b3be2130..7cac57bfa7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -264,6 +264,8 @@ abstract class Action extends DatabasesAction $relationId = $relation; } elseif (\is_array($relation) && \array_values($relation) !== $relation) { $relationId = $relation['$id'] ?? null; + } else { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Relationship value must be an object, document ID string, or associative array'); } if ($relationId !== null) { From cb66e5061252f4873d1f4f2cb0f84c68e38bc2f8 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:58:34 +0000 Subject: [PATCH 09/16] refactor: remove magic class strings --- app/controllers/general.php | 6 +- src/Appwrite/GraphQL/Types/Mapper.php | 106 +++++++-------- .../SDK/Specification/Format/OpenAPI3.php | 122 +++++++++--------- .../SDK/Specification/Format/Swagger2.php | 78 +++++------ 4 files changed, 156 insertions(+), 156 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 8fc5a11503..2bd0a6d54c 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1228,7 +1228,7 @@ App::error() } switch ($class) { - case 'Utopia\Exception': + case Utopia\Exception::class: $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); switch ($code) { case 400: @@ -1239,10 +1239,10 @@ App::error() break; } break; - case 'Utopia\Database\Exception\Authorization': + case Utopia\Database\Exception\Authorization::class: $error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED); break; - case 'Utopia\Database\Exception\Timeout': + case Utopia\Database\Exception\Timeout::class: $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); break; } diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index c9ae84f1c3..8935e67c0b 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -269,59 +269,59 @@ class Mapper } switch ((!empty($validator)) ? $validator::class : '') { - case 'Appwrite\Auth\Validator\Password': - case 'Appwrite\Event\Validator\Event': - case 'Appwrite\Event\Validator\FunctionEvent': - case 'Appwrite\Network\Validator\CNAME': - case 'Appwrite\Network\Validator\Email': - case 'Appwrite\Network\Validator\Redirect': - case 'Appwrite\Network\Validator\DNS': - case 'Appwrite\Network\Validator\Origin': - case 'Appwrite\Task\Validator\Cron': - case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Utopia\Database\Validator\Key': - case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Domain': - case 'Utopia\Validator\HexColor': - case 'Utopia\Validator\Host': - case 'Utopia\Validator\IP': - case 'Utopia\Validator\Origin': - case 'Utopia\Validator\Text': - case 'Utopia\Validator\URL': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Auth\Validator\Password::class: + case \Appwrite\Event\Validator\Event::class: + case \Appwrite\Event\Validator\FunctionEvent::class: + case \Appwrite\Network\Validator\CNAME::class: + case \Appwrite\Network\Validator\Email::class: + case \Appwrite\Network\Validator\Redirect::class: + case \Appwrite\Network\Validator\DNS::class: + case \Appwrite\Network\Validator\Origin::class: + case \Appwrite\Task\Validator\Cron::class: + case \Appwrite\Utopia\Database\Validator\CustomId::class: + case \Utopia\Database\Validator\Key::class: + case \Utopia\Database\Validator\UID::class: + case \Utopia\Validator\Domain::class: + case \Utopia\Validator\HexColor::class: + case \Utopia\Validator\Host::class: + case \Utopia\Validator\IP::class: + case \Utopia\Validator\Origin::class: + case \Utopia\Validator\Text::class: + case \Utopia\Validator\URL::class: + case \Utopia\Validator\WhiteList::class: default: $type = Type::string(); break; - case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': - case 'Appwrite\Utopia\Database\Validator\Queries\Collections': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Databases': - case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': - case 'Appwrite\Utopia\Database\Validator\Queries\Executions': - case 'Appwrite\Utopia\Database\Validator\Queries\Files': - case 'Appwrite\Utopia\Database\Validator\Queries\Functions': - case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': - case 'Appwrite\Utopia\Database\Validator\Queries\Installations': - case 'Appwrite\Utopia\Database\Validator\Queries\Memberships': - case 'Appwrite\Utopia\Database\Validator\Queries\Projects': - case 'Appwrite\Utopia\Database\Validator\Queries\Rules': - case 'Appwrite\Utopia\Database\Validator\Queries\Teams': - case 'Appwrite\Utopia\Database\Validator\Queries\Users': - case 'Appwrite\Utopia\Database\Validator\Queries\Variables': - case 'Utopia\Database\Validator\Authorization': - case 'Utopia\Database\Validator\Permissions': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Documents': - case 'Utopia\Database\Validator\Roles': + case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: + case \Appwrite\Utopia\Database\Validator\Queries\Collections::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Databases::class: + case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class: + case \Appwrite\Utopia\Database\Validator\Queries\Executions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Files::class: + case \Appwrite\Utopia\Database\Validator\Queries\Functions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Installations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class: + case \Appwrite\Utopia\Database\Validator\Queries\Projects::class: + case \Appwrite\Utopia\Database\Validator\Queries\Rules::class: + case \Appwrite\Utopia\Database\Validator\Queries\Teams::class: + case \Appwrite\Utopia\Database\Validator\Queries\Users::class: + case \Appwrite\Utopia\Database\Validator\Queries\Variables::class: + case \Utopia\Database\Validator\Authorization::class: + case \Utopia\Database\Validator\Permissions::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Documents::class: + case \Utopia\Database\Validator\Roles::class: $type = Type::listOf(Type::string()); break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $type = Type::boolean(); break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: $type = Type::listOf(self::param( $utopia, $validator->getValidator(), @@ -329,11 +329,11 @@ class Mapper $injections )); break; - case 'Utopia\Validator\Integer': - case 'Utopia\Validator\Numeric': + case \Utopia\Validator\Integer::class: + case \Utopia\Validator\Numeric::class: $type = Type::int(); break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: // Check if the Range validator is for float or integer if ($validator instanceof \Utopia\Validator\Range && $validator->getType() === \Utopia\Validator\Range::TYPE_FLOAT) { $type = Type::float(); @@ -341,16 +341,16 @@ class Mapper $type = Type::int(); } break; - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\FloatValidator::class: $type = Type::float(); break; - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\Assoc::class: $type = Types::assoc(); break; - case 'Utopia\Validator\JSON': + case \Utopia\Validator\JSON::class: $type = Types::json(); break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $type = Types::inputFile(); break; } diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 8171a45db4..8e710cd0ac 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -392,51 +392,51 @@ class OpenAPI3 extends Format : ''; switch ($base) { - case 'Appwrite\Utopia\Database\Validator\Queries\Base': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: $class = $base; break; } - if ($class === 'Utopia\Validator\AnyOf') { + if ($class === \Utopia\Validator\AnyOf::class) { $validator = $param['validator']->getValidators()[0]; $class = \get_class($validator); } $array = false; - if ($class === 'Utopia\Validator\ArrayList') { + if ($class === \Utopia\Validator\ArrayList::class) { $array = true; $subclass = \get_class($validator->getValidator()); switch ($subclass) { - case 'Appwrite\Utopia\Database\Validator\Operation': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Utopia\Database\Validator\Operation::class: + case \Utopia\Validator\WhiteList::class: $class = $subclass; break; } } switch ($class) { - case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Text': + case \Utopia\Database\Validator\UID::class: + case \Utopia\Validator\Text::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: false; break; - case 'Appwrite\Utopia\Database\Validator\CustomId': + case \Appwrite\Utopia\Database\Validator\CustomId::class: if ($sdk->getType() === MethodType::UPLOAD) { $node['schema']['x-upload-id'] = true; } $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Database\Validator\DatetimeValidator': + case \Utopia\Database\Validator\DatetimeValidator::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'datetime'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE; break; - case 'Utopia\Database\Validator\Spatial': + case \Utopia\Database\Validator\Spatial::class: /** @var Spatial $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -450,31 +450,31 @@ class OpenAPI3 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case \Appwrite\Network\Validator\Email::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'email'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': - case 'Appwrite\Network\Validator\Redirect': + case \Utopia\Validator\Host::class: + case \Utopia\Validator\URL::class: + case \Appwrite\Network\Validator\Redirect::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com'; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\JSON::class: + case \Utopia\Validator\Mock::class: + case \Utopia\Validator\Assoc::class: $param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['schema']['type'] = 'object'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '{}'; break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $consumes = ['multipart/form-data']; $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'binary'; break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: /** @var ArrayList $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -484,92 +484,92 @@ class OpenAPI3 extends Format $node['schema']['x-example'] = $param['example']; } break; - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': - case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': - case 'Appwrite\Utopia\Database\Validator\Queries\Collections': - case 'Appwrite\Utopia\Database\Validator\Queries\Databases': - case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': - case 'Appwrite\Utopia\Database\Validator\Queries\Executions': - case 'Appwrite\Utopia\Database\Validator\Queries\Files': - case 'Appwrite\Utopia\Database\Validator\Queries\Functions': - case 'Appwrite\Utopia\Database\Validator\Queries\Identities': - case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': - case 'Appwrite\Utopia\Database\Validator\Queries\Installations': - case 'Appwrite\Utopia\Database\Validator\Queries\Memberships': - case 'Appwrite\Utopia\Database\Validator\Queries\Messages': - case 'Appwrite\Utopia\Database\Validator\Queries\Migrations': - case 'Appwrite\Utopia\Database\Validator\Queries\Projects': - case 'Appwrite\Utopia\Database\Validator\Queries\Providers': - case 'Appwrite\Utopia\Database\Validator\Queries\Rules': - case 'Appwrite\Utopia\Database\Validator\Queries\Subscribers': - case 'Appwrite\Utopia\Database\Validator\Queries\Targets': - case 'Appwrite\Utopia\Database\Validator\Queries\Teams': - case 'Appwrite\Utopia\Database\Validator\Queries\Topics': - case 'Appwrite\Utopia\Database\Validator\Queries\Users': - case 'Appwrite\Utopia\Database\Validator\Queries\Variables': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Document': - case 'Utopia\Database\Validator\Queries\Documents': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: + case \Appwrite\Utopia\Database\Validator\Queries\Collections::class: + case \Appwrite\Utopia\Database\Validator\Queries\Databases::class: + case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class: + case \Appwrite\Utopia\Database\Validator\Queries\Executions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Files::class: + case \Appwrite\Utopia\Database\Validator\Queries\Functions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Identities::class: + case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Installations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class: + case \Appwrite\Utopia\Database\Validator\Queries\Messages::class: + case \Appwrite\Utopia\Database\Validator\Queries\Migrations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Projects::class: + case \Appwrite\Utopia\Database\Validator\Queries\Providers::class: + case \Appwrite\Utopia\Database\Validator\Queries\Rules::class: + case \Appwrite\Utopia\Database\Validator\Queries\Subscribers::class: + case \Appwrite\Utopia\Database\Validator\Queries\Targets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Teams::class: + case \Appwrite\Utopia\Database\Validator\Queries\Topics::class: + case \Appwrite\Utopia\Database\Validator\Queries\Users::class: + case \Appwrite\Utopia\Database\Validator\Queries\Variables::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Document::class: + case \Utopia\Database\Validator\Queries\Documents::class: $node['schema']['type'] = 'array'; $node['schema']['items'] = [ 'type' => 'string', ]; break; - case 'Utopia\Database\Validator\Permissions': + case \Utopia\Database\Validator\Permissions::class: $node['schema']['type'] = $validator->getType(); $node['schema']['items'] = [ 'type' => 'string', ]; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]'; break; - case 'Utopia\Database\Validator\Roles': + case \Utopia\Database\Validator\Roles::class: $node['schema']['type'] = $validator->getType(); $node['schema']['items'] = [ 'type' => 'string', ]; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]'; break; - case 'Appwrite\Auth\Validator\Password': + case \Appwrite\Auth\Validator\Password::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'password'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'password'; break; - case 'Appwrite\Auth\Validator\Phone': + case \Appwrite\Auth\Validator\Phone::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'phone'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '+12065550100'; // In the US, 555 is reserved like example.com break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: /** @var Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: $validator->getMin(); break; - case 'Utopia\Validator\Integer': + case \Utopia\Validator\Integer::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = $validator->getFormat(); if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\Numeric::class: + case \Utopia\Validator\FloatValidator::class: $node['schema']['type'] = 'number'; $node['schema']['format'] = 'float'; if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Length': + case \Utopia\Validator\Length::class: $node['schema']['type'] = $validator->getType(); if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\WhiteList': + case \Utopia\Validator\WhiteList::class: if ($array) { $validator = $validator->getValidator(); @@ -687,11 +687,11 @@ class OpenAPI3 extends Format } } break; - case 'Appwrite\Utopia\Database\Validator\CompoundUID': + case \Appwrite\Utopia\Database\Validator\CompoundUID::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: ''; break; - case 'Appwrite\Utopia\Database\Validator\Operation': + case \Appwrite\Utopia\Database\Validator\Operation::class: if ($array) { $validator = $validator->getValidator(); } diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 990c456851..0d66e4d725 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -397,51 +397,51 @@ class Swagger2 extends Format : ''; switch ($base) { - case 'Appwrite\Utopia\Database\Validator\Queries\Base': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: $class = $base; break; } - if ($class === 'Utopia\Validator\AnyOf') { + if ($class === \Utopia\Validator\AnyOf::class) { $validator = $param['validator']->getValidators()[0]; $class = \get_class($validator); } $array = false; - if ($class === 'Utopia\Validator\ArrayList') { + if ($class === \Utopia\Validator\ArrayList::class) { $array = true; $subclass = \get_class($validator->getValidator()); switch ($subclass) { - case 'Appwrite\Utopia\Database\Validator\Operation': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Utopia\Database\Validator\Operation::class: + case \Utopia\Validator\WhiteList::class: $class = $subclass; break; } } switch ($class) { - case 'Utopia\Validator\Text': - case 'Utopia\Database\Validator\UID': + case \Utopia\Validator\Text::class: + case \Utopia\Database\Validator\UID::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: false; break; - case 'Appwrite\Utopia\Database\Validator\CustomId': + case \Appwrite\Utopia\Database\Validator\CustomId::class: if ($sdk->getType() === MethodType::UPLOAD) { $node['x-upload-id'] = true; } $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Database\Validator\DatetimeValidator': + case \Utopia\Database\Validator\DatetimeValidator::class: $node['type'] = $validator->getType(); $node['format'] = 'datetime'; $node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE; break; - case 'Utopia\Database\Validator\Spatial': + case \Utopia\Database\Validator\Spatial::class: /** @var Spatial $validator */ $node['type'] = 'array'; $node['schema']['items'] = [ @@ -455,19 +455,19 @@ class Swagger2 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case \Appwrite\Network\Validator\Email::class: $node['type'] = $validator->getType(); $node['format'] = 'email'; $node['x-example'] = ($param['example'] ?? '') ?: 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': - case 'Appwrite\Network\Validator\Redirect': + case \Utopia\Validator\Host::class: + case \Utopia\Validator\URL::class: + case \Appwrite\Network\Validator\Redirect::class: $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = ($param['example'] ?? '') ?: 'https://example.com'; break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: /** @var ArrayList $validator */ $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; @@ -478,34 +478,34 @@ class Swagger2 extends Format $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\JSON::class: + case \Utopia\Validator\Mock::class: + case \Utopia\Validator\Assoc::class: $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = ($param['example'] ?? '') ?: '{}'; break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $consumes = ['multipart/form-data']; $node['type'] = 'file'; break; - case 'Appwrite\Functions\Validator\Payload': + case \Appwrite\Functions\Validator\Payload::class: $consumes = ['multipart/form-data']; $node['type'] = 'payload'; break; - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Document': - case 'Utopia\Database\Validator\Queries\Documents': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Document::class: + case \Utopia\Database\Validator\Queries\Documents::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; $node['items'] = [ 'type' => 'string', ]; break; - case 'Utopia\Database\Validator\Permissions': + case \Utopia\Database\Validator\Permissions::class: $node['type'] = $validator->getType(); $node['collectionFormat'] = 'multi'; $node['items'] = [ @@ -513,7 +513,7 @@ class Swagger2 extends Format ]; $node['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]'; break; - case 'Utopia\Database\Validator\Roles': + case \Utopia\Database\Validator\Roles::class: $node['type'] = $validator->getType(); $node['collectionFormat'] = 'multi'; $node['items'] = [ @@ -521,44 +521,44 @@ class Swagger2 extends Format ]; $node['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]'; break; - case 'Appwrite\Auth\Validator\Password': + case \Appwrite\Auth\Validator\Password::class: $node['type'] = $validator->getType(); $node['format'] = 'password'; $node['x-example'] = ($param['example'] ?? '') ?: 'password'; break; - case 'Appwrite\Auth\Validator\Phone': + case \Appwrite\Auth\Validator\Phone::class: $node['type'] = $validator->getType(); $node['format'] = 'phone'; $node['x-example'] = ($param['example'] ?? '') ?: '+12065550100'; break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: /** @var Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = ($param['example'] ?? '') ?: $validator->getMin(); break; - case 'Utopia\Validator\Integer': + case \Utopia\Validator\Integer::class: $node['type'] = $validator->getType(); $node['format'] = $validator->getFormat(); if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\Numeric::class: + case \Utopia\Validator\FloatValidator::class: $node['type'] = 'number'; $node['format'] = 'float'; if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Length': + case \Utopia\Validator\Length::class: $node['type'] = $validator->getType(); if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\WhiteList': + case \Utopia\Validator\WhiteList::class: if ($array) { $validator = $validator->getValidator(); @@ -665,11 +665,11 @@ class Swagger2 extends Format } } break; - case 'Appwrite\Utopia\Database\Validator\CompoundUID': + case \Appwrite\Utopia\Database\Validator\CompoundUID::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: ''; break; - case 'Appwrite\Utopia\Database\Validator\Operation': + case \Appwrite\Utopia\Database\Validator\Operation::class: if ($array) { $validator = $validator->getValidator(); } From 7f3ea98924c6aa9977f9eee9182f3c3d01a8feb1 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 27 Jan 2026 13:00:29 +0000 Subject: [PATCH 10/16] refactor: use array_is_list() and assertEventually helper --- .../Http/Databases/Collections/Documents/Action.php | 2 +- .../Services/Databases/TablesDB/DatabasesBase.php | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 7cac57bfa7..c0a95ce0bd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -262,7 +262,7 @@ abstract class Action extends DatabasesAction $relationId = $relation->getId(); } elseif (\is_string($relation)) { $relationId = $relation; - } elseif (\is_array($relation) && \array_values($relation) !== $relation) { + } elseif (\is_array($relation) && !\array_is_list($relation)) { $relationId = $relation['$id'] ?? null; } else { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Relationship value must be an object, document ID string, or associative array'); diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index 8d8133241b..62b7851271 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7693,22 +7693,15 @@ trait DatabasesBase $this->assertEquals(202, $relationship['headers']['status-code']); // Wait for relationship column to be available - $maxAttempts = 10; - $childrenFound = false; - for ($i = 0; $i < $maxAttempts; $i++) { + $this->assertEventually(function () use ($databaseId, $parentTableId) { $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ])); $columnKeys = array_column($columns['body']['columns'], 'key'); - if (in_array('children', $columnKeys)) { - $childrenFound = true; - break; - } - usleep(200000); - } - $this->assertTrue($childrenFound, "Relationship column 'children' not found in table {$parentTableId} of database {$databaseId}"); + $this->assertContains('children', $columnKeys, "Relationship column 'children' not found in table {$parentTableId} of database {$databaseId}"); + }, 2000, 200); // ID too long (>36 chars) should fail $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ From aef7b8df38e1760bee332cb9d64c101ce7b94e18 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 08:41:10 +0000 Subject: [PATCH 11/16] fix: use RELATIONSHIP_VALUE_INVALID exception for validation errors --- .../Http/Databases/Collections/Documents/Action.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index c0a95ce0bd..1df947f8c3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -265,13 +265,16 @@ abstract class Action extends DatabasesAction } elseif (\is_array($relation) && !\array_is_list($relation)) { $relationId = $relation['$id'] ?? null; } else { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Relationship value must be an object, document ID string, or associative array'); + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object, document ID string, or associative array'); } if ($relationId !== null) { + if (!\is_string($relationId)) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship $id must be a string'); + } $validator = new CustomId(); if (!$validator->isValid($relationId)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $validator->getDescription()); } } } From cbe2d2383d1310c55d94b2fe7c9692aac5b2f2e7 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 10:22:00 +0000 Subject: [PATCH 12/16] chore: update phpunit to 9.6.34 (security fix) --- composer.lock | 53 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/composer.lock b/composer.lock index db1096fee8..0ac488570a 100644 --- a/composer.lock +++ b/composer.lock @@ -5564,30 +5564,29 @@ }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -5614,7 +5613,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -5630,7 +5629,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "doctrine/lexer", @@ -6664,16 +6663,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.31", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -6695,7 +6694,7 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", "sebastian/exporter": "^4.0.8", @@ -6747,7 +6746,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -6771,7 +6770,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T07:45:52+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/cache", @@ -6991,16 +6990,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -7053,7 +7052,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { @@ -7073,7 +7072,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -8943,7 +8942,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8967,5 +8966,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From 2f3fa9e0d3138704b48e7c09a5aa4adc61377348 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 11:42:51 +0000 Subject: [PATCH 13/16] sync CONTRIBUTING.md with 1.8.x --- CONTRIBUTING.md | 78 ++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96b0614165..c6837673d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,73 +222,51 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi ```bash . ├── app # Main application -│ ├── assets -│ │ ├── dbip -│ │ ├── fonts -│ │ └── security │ ├── config # Config files -│ │ ├── avatars -│ │ ├── collections -│ │ ├── locale -│ │ ├── specs -│ │ ├── storage -│ │ └── templates │ ├── controllers # API & dashboard controllers │ │ ├── api │ │ ├── shared │ │ └── web -│ ├── init # DB schemas -│ │ └── database -│ └── views # HTML server-side templates -│ ├── general -│ └── install +│ ├── db # DB schemas +│ ├── sdks # SDKs generated copies (used for generating code examples) +│ ├── tasks # Server CLI commands +│ ├── views # HTML server-side templates +│ └── workers # Background workers ├── bin # Server executables (tasks & workers) -├── dev # Debugger config +├── docker # Docker related resources and configs ├── docs # Docs and tutorials │ ├── examples -│ ├── lists │ ├── references -│ ├── sdks │ ├── services │ ├── specs │ └── tutorials ├── public # Public files +│ ├── dist │ ├── fonts │ ├── images -│ ├── sdk-console -│ ├── sdk-project -│ └── sdk-web -├── src # Supporting libraries (each lib has one role, common libs are released as -│ ├── Appwrite -│ │ ├── Auth -│ │ ├── Certificates -│ │ ├── Deletes -│ │ ├── Detector -│ │ ├── Docker -│ │ ├── Event -│ │ ├── Extend -│ │ ├── Functions/Validator -│ │ ├── GraphQL -│ │ ├── Hooks -│ │ ├── Messaging -│ │ ├── Migration -│ │ ├── Network -│ │ ├── OpenSSL -│ │ ├── Platform -│ │ ├── Promises -│ │ ├── PubSub -│ │ ├── SDK -│ │ ├── Task/Validator -│ │ ├── Template -│ │ ├── Transformation -│ │ ├── URL -│ │ ├── Utopia -│ │ └── Vcs -│ └── Executor +│ ├── scripts +│ └── styles +├── src # Supporting libraries (each lib has one role, common libs are released as individual projects) +│ └── Appwrite +│ ├── Auth +│ ├── Detector +│ ├── Docker +| ├── DSN +│ ├── Event +│ ├── Extend +│ ├── GraphQL +│ ├── Messaging +│ ├── Migration +│ ├── Network +│ ├── OpenSSL +│ ├── Promises +│ ├── Specification +│ ├── Task +│ ├── Template +│ ├── URL +│ └── Utopia └── tests # End to end & unit tests - ├── benchmarks ├── e2e - ├── extensions ├── resources └── unit ``` From 14a96a2b56a79c665de8bc4a9f5e75d9864fd02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 28 Jan 2026 14:50:17 +0100 Subject: [PATCH 14/16] Remove unnessessary attributes --- app/config/collections/platform.php | 24 ------------------------ app/controllers/api/projects.php | 3 --- app/controllers/mock.php | 3 --- 3 files changed, 30 deletions(-) diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 73c9eea870..2fb3168c5b 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -632,30 +632,6 @@ $platformCollections = [ '$id' => ID::custom('keys'), 'name' => 'keys', 'attributes' => [ - // Delete eventuelly, when removing dual-write too - [ - '$id' => ID::custom('projectInternalId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - // Delete eventuelly, when removing dual-write too - [ - '$id' => ID::custom('projectId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => 0, - 'array' => false, - 'filters' => [], - ], [ '$id' => ID::custom('resourceType'), 'type' => Database::VAR_STRING, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 1e03c861d1..57ad3030d9 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1502,9 +1502,6 @@ App::post('/v1/projects/:projectId/keys') Permission::update(Role::any()), Permission::delete(Role::any()), ], - // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), 'resourceId' => $project->getId(), 'resourceType' => 'projects', diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 16d6d72de7..42b300e410 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -200,9 +200,6 @@ App::post('/v1/mock/api-key-unprefixed') Permission::update(Role::any()), Permission::delete(Role::any()), ], - // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), 'resourceId' => $project->getId(), 'resourceType' => 'projects', From e22e8d6a5fe617ed76d53d2d5565854c36addcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 28 Jan 2026 14:55:13 +0100 Subject: [PATCH 15/16] Upgrade phpunit for vuln --- composer.lock | 80 +++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/composer.lock b/composer.lock index bd56277819..1c7e6c2a5b 100644 --- a/composer.lock +++ b/composer.lock @@ -2066,16 +2066,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.48", + "version": "3.0.49", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", "shasum": "" }, "require": { @@ -2156,7 +2156,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" }, "funding": [ { @@ -2172,7 +2172,7 @@ "type": "tidelift" } ], - "time": "2025-12-15T11:51:42+00:00" + "time": "2026-01-27T09:17:28+00:00" }, { "name": "psr/container", @@ -2735,16 +2735,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "d63c23357d74715a589454c141c843f0172bec6c" + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c", - "reference": "d63c23357d74715a589454c141c843f0172bec6c", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", "shasum": "" }, "require": { @@ -2812,7 +2812,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.5" }, "funding": [ { @@ -2832,7 +2832,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T16:34:22+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-client-contracts", @@ -5120,28 +5120,28 @@ }, { "name": "utopia-php/swoole", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "95a937acb393dbf95cccba239d55886e2848ab0b" + "reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/95a937acb393dbf95cccba239d55886e2848ab0b", - "reference": "95a937acb393dbf95cccba239d55886e2848ab0b", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95", + "reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95", "shasum": "" }, "require": { - "ext-swoole": "*", - "php": ">=8.0", + "ext-swoole": "6.*", + "php": ">=8.1", "utopia-php/framework": "0.33.37" }, "require-dev": { "laravel/pint": "1.2.*", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "5.0.2" + "swoole/ide-helper": "6.0.2" }, "type": "library", "autoload": { @@ -5165,9 +5165,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/1.0.0" + "source": "https://github.com/utopia-php/swoole/tree/1.0.1" }, - "time": "2026-01-14T14:00:11+00:00" + "time": "2026-01-28T12:43:38+00:00" }, { "name": "utopia-php/system", @@ -6772,16 +6772,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.32", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "492ee10a8369a1c1ac390a3b46e0c846e384c5a4" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/492ee10a8369a1c1ac390a3b46e0c846e384c5a4", - "reference": "492ee10a8369a1c1ac390a3b46e0c846e384c5a4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -6855,7 +6855,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.32" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -6879,7 +6879,7 @@ "type": "tidelift" } ], - "time": "2026-01-24T16:04:20+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/cache", @@ -8199,16 +8199,16 @@ }, { "name": "symfony/finder", - "version": "v8.0.4", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1" + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1", - "reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1", + "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", "shasum": "" }, "require": { @@ -8243,7 +8243,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.4" + "source": "https://github.com/symfony/finder/tree/v8.0.5" }, "funding": [ { @@ -8263,7 +8263,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/options-resolver", @@ -8668,16 +8668,16 @@ }, { "name": "symfony/process", - "version": "v8.0.4", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "10df72602d88c0a3fa685b822976a052611dd607" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607", - "reference": "10df72602d88c0a3fa685b822976a052611dd607", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { @@ -8709,7 +8709,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.4" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -8729,7 +8729,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T11:07:10+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/string", @@ -9075,5 +9075,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 23dae85d2f409a2972207ffa3b17c969189e8be8 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 18:45:11 +0000 Subject: [PATCH 16/16] Sync composer.lock with 1.8.x --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index fa87eebe72..1c7e6c2a5b 100644 --- a/composer.lock +++ b/composer.lock @@ -9051,7 +9051,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": {