From 80ad740d36f9529cf9653baacae4ec0f6bae1caf Mon Sep 17 00:00:00 2001 From: Tejas Raskar Date: Wed, 20 Aug 2025 14:34:43 +0530 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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 7f3ea98924c6aa9977f9eee9182f3c3d01a8feb1 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 27 Jan 2026 13:00:29 +0000 Subject: [PATCH 09/13] 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 10/13] 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 11/13] 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 12/13] 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 23dae85d2f409a2972207ffa3b17c969189e8be8 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 28 Jan 2026 18:45:11 +0000 Subject: [PATCH 13/13] 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": {