From f0c10acbb445a0eede176c7b659b3784c9f67028 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 19:30:59 +1200 Subject: [PATCH 01/22] Fix readonly attr stripping on write --- .../Collections/Documents/Action.php | 9 +-- .../Collections/Documents/Bulk/Update.php | 3 +- .../Collections/Documents/Bulk/Upsert.php | 1 + .../Collections/Documents/Create.php | 9 ++- .../Collections/Documents/Update.php | 11 +-- .../Collections/Documents/Upsert.php | 5 +- .../Databases/Legacy/DatabasesBase.php | 69 +++++++++++++++++++ 7 files changed, 90 insertions(+), 17 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 78df15b0c1..e077545b11 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 @@ -27,9 +27,8 @@ abstract class Action extends AppwriteAction $this->context = ROWS; } - // Use the same helper method to ensure consistency $contextId = '$' . $this->getCollectionsEventsContext() . 'Id'; - $this->removableAttributes = ['$databaseId', $contextId, '$sequence']; + $this->removableAttributes = ['$sequence', '$databaseId', $contextId]; return parent::setHttpPath($path); } @@ -200,11 +199,13 @@ abstract class Action extends AppwriteAction * Remove configured removable attributes from a document. * Used for relationship path handling to remove API-specific attributes. */ - protected function removeReadonlyAttributes(Document $document): void + protected function removeReadonlyAttributes(Document|array $document): Document|array { foreach ($this->removableAttributes as $attribute) { - $document->removeAttribute($attribute); + \var_dump('Removing attribute: ' . $attribute); + unset($document[$attribute]); } + return $document; } /** diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php index a9f9c3f76d..226303c657 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php @@ -127,8 +127,7 @@ class Update extends Action } } - // Remove sequence if set - unset($document['$sequence']); + $data = $this->removeReadonlyAttributes($data); $documents = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index a6f27637e3..6b9a81cd69 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -100,6 +100,7 @@ class Upsert extends Action } foreach ($documents as $key => $document) { + $document = $this->removeReadonlyAttributes($document); $documents[$key] = new Document($document); } 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 04c90c4ec1..4bd0c6cdee 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 @@ -298,6 +298,8 @@ class Create extends Action ); foreach ($relations as &$relation) { + $relation = $this->removeReadonlyAttributes($relation); + if ( \is_array($relation) && \array_values($relation) !== $relation @@ -318,7 +320,6 @@ class Create extends Action $relation['$id'] = ID::unique(); } } else { - $this->removeReadonlyAttributes($relation); $relation->setAttribute('$collection', $relatedCollection->getId()); $type = Database::PERMISSION_UPDATE; } @@ -351,9 +352,6 @@ class Create extends Action } } - // Remove sequence if set - unset($document['$sequence']); - // Assign a unique ID if needed, otherwise use the provided ID. $document['$id'] = $sourceId === 'unique()' ? ID::unique() : $sourceId; @@ -368,10 +366,11 @@ class Create extends Action } } + $document = $this->removeReadonlyAttributes($document); + $document = new Document($document); $setPermissions($document, $permissions); $checkPermissions($collection, $document, Database::PERMISSION_CREATE); - return $document; }, $documents); 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 334bcb8448..fd86fa995c 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 @@ -159,17 +159,14 @@ class Update extends Action $permissions = $document->getPermissions() ?? []; } - // Remove sequence if set - unset($document['$sequence']); - $data['$id'] = $documentId; $data['$permissions'] = $permissions; + $data = $this->removeReadonlyAttributes($data); $newDocument = new Document($data); $operations = 0; $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { - $operations++; $relationships = \array_filter( @@ -198,6 +195,8 @@ class Update extends Action ); foreach ($relations as &$relation) { + $relation = $this->removeReadonlyAttributes($relation); + // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) @@ -212,7 +211,7 @@ class Update extends Action 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); - $this->removeReadonlyAttributes($relation); + // Attribute $collection is required for Utopia. $relation->setAttribute( '$collection', @@ -242,6 +241,8 @@ class Update extends Action ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations); + \var_dump($newDocument); + try { $document = $dbForProject->withRequestTimestamp( $requestTimestamp, 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 6027a20c41..622b43db2a 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 @@ -170,6 +170,7 @@ class Upsert extends Action $data['$id'] = $documentId; $data['$permissions'] = $permissions ?? []; + $data = $this->removeReadonlyAttributes($data); $newDocument = new Document($data); $operations = 0; @@ -203,6 +204,8 @@ class Upsert extends Action ); foreach ($relations as &$relation) { + $relation = $this->removeReadonlyAttributes($relation); + // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) @@ -217,7 +220,7 @@ class Upsert extends Action 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); - $this->removeReadonlyAttributes($relation); + // Attribute $collection is required for Utopia. $relation->setAttribute( '$collection', diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index bd71272537..0df0a2f5af 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -1697,6 +1697,7 @@ trait DatabasesBase return $data; } + /** * @depends testCreateIndexes */ @@ -2211,6 +2212,49 @@ trait DatabasesBase $this->assertArrayHasKey('$permissions', $library3['body']); $this->assertCount(3, $library3['body']['$permissions']); $this->assertNotEmpty($library3['body']['$permissions']); + + // Readonly attributes are ignored + $personNoPerm = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + '$id' => 'some-other-id', + '$collectionId' => 'some-other-collection', + '$databaseId' => 'some-other-database', + '$createdAt' => '2024-01-01T00:00:00Z', + '$updatedAt' => '2024-01-01T00:00:00Z', + 'library' => [ + '$id' => 'library3', + 'libraryName' => 'Library 3', + '$createdAt' => '2024-01-01T00:00:00Z', + '$updatedAt' => '2024-01-01T00:00:00Z', + ], + ], + ]); + + $update = $personNoPerm; + $update['body']['$id'] = 'random'; + $update['body']['$sequence'] = 123; + $update['body']['$databaseId'] = 'random'; + $update['body']['$collectionId'] = 'random'; + $update['body']['$createdAt'] = '2024-01-01T00:00:00Z'; + $update['body']['$updatedAt'] = '2024-01-01T00:00:00Z'; + + $upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => $update['body'] + ]); + + $this->assertEquals(200, $upserted['headers']['status-code']); + $this->assertEquals($personNoPerm['body']['$id'], $upserted['body']['$id']); + $this->assertEquals($personNoPerm['body']['$collectionId'], $upserted['body']['$collectionId']); + $this->assertEquals($personNoPerm['body']['$databaseId'], $upserted['body']['$databaseId']); + $this->assertEquals($personNoPerm['body']['$sequence'], $upserted['body']['$sequence']); + $this->assertEquals($personNoPerm['body']['$createdAt'], $upserted['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00Z', $upserted['body']['$updatedAt']); } } @@ -3000,6 +3044,31 @@ trait DatabasesBase $this->assertEquals(200, $response['headers']['status-code']); + // Test readonly attributes are ignored + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-timestamp' => DateTime::formatTz(DateTime::now()), + ], $this->getHeaders()), [ + 'data' => [ + '$id' => 'newId', + '$sequence' => 9999, + '$collectionId' => 'newCollectionId', + '$databaseId' => 'newDatabaseId', + '$createdAt' => '2024-01-01T00:00:00+00:00', + '$updatedAt' => '2024-01-01T00:00:00+00:00', + 'title' => 'Thor: Ragnarok', + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($id, $response['body']['$id']); + $this->assertEquals($data['moviesId'], $response['body']['$collectionId']); + $this->assertEquals($databaseId, $response['body']['$databaseId']); + $this->assertNotEquals('2024-01-01T00:00:00+00:00', $response['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00+00:00', $response['body']['$updatedAt']); + $this->assertNotEquals(9999, $response['body']['$sequence']); + return []; } From d9aadb02e7a316c2743a24e0a4283a02859fe7fd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 19:53:47 +1200 Subject: [PATCH 02/22] Move filter --- .../Databases/Http/Databases/Collections/Documents/Create.php | 4 ++-- .../Databases/Http/Databases/Collections/Documents/Delete.php | 1 + .../Databases/Http/Databases/Collections/Documents/Update.php | 4 ++-- .../Databases/Http/Databases/Collections/Documents/Upsert.php | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) 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 4bd0c6cdee..6222c6c183 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 @@ -298,8 +298,6 @@ class Create extends Action ); foreach ($relations as &$relation) { - $relation = $this->removeReadonlyAttributes($relation); - if ( \is_array($relation) && \array_values($relation) !== $relation @@ -309,6 +307,8 @@ class Create extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation); + $current = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId()) ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 4ae1624f73..f34b4630c2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -117,6 +117,7 @@ class Delete extends Action } $collectionsCache = []; + $this->processDocument( database: $database, collection: $collection, 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 fd86fa995c..7512b1aab0 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 @@ -195,8 +195,6 @@ class Update extends Action ); foreach ($relations as &$relation) { - $relation = $this->removeReadonlyAttributes($relation); - // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) @@ -207,6 +205,8 @@ class Update extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation); + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() 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 622b43db2a..004777c1b2 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 @@ -204,8 +204,6 @@ class Upsert extends Action ); foreach ($relations as &$relation) { - $relation = $this->removeReadonlyAttributes($relation); - // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) @@ -216,6 +214,8 @@ class Upsert extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation); + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() From 57071af3e87857eb86dcb0376276e3b6e62a39bf Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 20:38:55 +1200 Subject: [PATCH 03/22] Handle privileged --- .../Collections/Documents/Action.php | 25 ++++++++++++++--- .../Collections/Documents/Bulk/Update.php | 2 +- .../Collections/Documents/Bulk/Upsert.php | 2 +- .../Collections/Documents/Create.php | 19 ++----------- .../Collections/Documents/Update.php | 16 ++--------- .../Collections/Documents/Upsert.php | 20 ++++--------- .../Databases/Collections/Documents/XList.php | 4 +-- src/Appwrite/Platform/Workers/Migrations.php | 1 - .../Databases/Legacy/DatabasesBase.php | 28 +++++++++++++------ 9 files changed, 56 insertions(+), 61 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 e077545b11..c952b3bc6e 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 @@ -28,7 +28,17 @@ abstract class Action extends AppwriteAction } $contextId = '$' . $this->getCollectionsEventsContext() . 'Id'; - $this->removableAttributes = ['$sequence', '$databaseId', $contextId]; + $this->removableAttributes = [ + '*' => [ + '$sequence', + '$databaseId', + $contextId, + ], + 'privileged' => [ + '$createdAt', + '$updatedAt', + ], + ]; return parent::setHttpPath($path); } @@ -199,12 +209,19 @@ abstract class Action extends AppwriteAction * Remove configured removable attributes from a document. * Used for relationship path handling to remove API-specific attributes. */ - protected function removeReadonlyAttributes(Document|array $document): Document|array + protected function removeReadonlyAttributes( + Document|array $document, + bool $privileged = false, + ): Document|array { - foreach ($this->removableAttributes as $attribute) { - \var_dump('Removing attribute: ' . $attribute); + foreach ($this->removableAttributes['*'] as $attribute) { unset($document[$attribute]); } + if (!$privileged) { + foreach ($this->removableAttributes['privileged'] ?? [] as $attribute) { + unset($document[$attribute]); + } + } return $document; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php index 226303c657..0f0ae14020 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php @@ -127,7 +127,7 @@ class Update extends Action } } - $data = $this->removeReadonlyAttributes($data); + $data = $this->removeReadonlyAttributes($data, privileged: true); $documents = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index 6b9a81cd69..3c6e5ddc57 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -100,7 +100,7 @@ class Upsert extends Action } foreach ($documents as $key => $document) { - $document = $this->removeReadonlyAttributes($document); + $document = $this->removeReadonlyAttributes($document, privileged: true); $documents[$key] = new Document($document); } 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 6222c6c183..d274e1f128 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 @@ -254,7 +254,7 @@ class Create extends Action $operations = 0; - $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use ($isAPIKey, $isPrivilegedUser, &$checkPermissions, $dbForProject, $database, &$operations) { $operations++; $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -307,7 +307,7 @@ class Create extends Action $relation = new Document($relation); } if ($relation instanceof Document) { - $relation = $this->removeReadonlyAttributes($relation); + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $current = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId()) @@ -354,20 +354,7 @@ class Create extends Action // Assign a unique ID if needed, otherwise use the provided ID. $document['$id'] = $sourceId === 'unique()' ? ID::unique() : $sourceId; - - // Allowing to add createdAt and updatedAt timestamps if server side(api key - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($document['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($document['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } - - $document = $this->removeReadonlyAttributes($document); - + $document = $this->removeReadonlyAttributes($document, $isAPIKey || $isPrivilegedUser); $document = new Document($document); $setPermissions($document, $permissions); $checkPermissions($collection, $document, Database::PERMISSION_CREATE); 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 7512b1aab0..556d14219f 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 @@ -109,16 +109,6 @@ class Update extends Action throw new Exception($this->getParentNotFoundException()); } - // Allowing to add createdAt and updatedAt timestamps if server side(api key) - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($data['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($data['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } // Read permission should not be required for update /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); @@ -161,12 +151,12 @@ class Update extends Action $data['$id'] = $documentId; $data['$permissions'] = $permissions; - $data = $this->removeReadonlyAttributes($data); + $data = $this->removeReadonlyAttributes($data, $isAPIKey || $isPrivilegedUser); $newDocument = new Document($data); $operations = 0; - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { $operations++; $relationships = \array_filter( @@ -205,7 +195,7 @@ class Update extends Action $relation = new Document($relation); } if ($relation instanceof Document) { - $relation = $this->removeReadonlyAttributes($relation); + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), 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 004777c1b2..54b1cad950 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 @@ -121,7 +121,8 @@ class Upsert extends Action ]; $permissions = Permission::aggregate($permissions, $allowedPermissions); - // if no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario) + + // If no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario) if (\is_null($permissions)) { $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); if ($oldDocument->isEmpty()) { @@ -157,25 +158,14 @@ class Upsert extends Action } } } - // Allowing to add createdAt and updatedAt timestamps if server side(api key) - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($data['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($data['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } $data['$id'] = $documentId; $data['$permissions'] = $permissions ?? []; - $data = $this->removeReadonlyAttributes($data); + $data = $this->removeReadonlyAttributes($data, $isAPIKey || $isPrivilegedUser); $newDocument = new Document($data); $operations = 0; - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { - + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { $operations++; $relationships = \array_filter( @@ -214,7 +204,7 @@ class Upsert extends Action $relation = new Document($relation); } if ($relation instanceof Document) { - $relation = $this->removeReadonlyAttributes($relation); + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 57ca550c18..9c8405cf18 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -176,7 +176,7 @@ class XList extends Action } // Check which removable attributes are explicitly requested - foreach ($this->removableAttributes as $attribute) { + foreach ($this->removableAttributes['*'] as $attribute) { if (\in_array($attribute, $values, true)) { $requestedAttributes[$attribute] = true; } @@ -186,7 +186,7 @@ class XList extends Action if (!$hasWildcard) { foreach ($documents as $document) { // Remove attributes that are not explicitly requested - foreach ($this->removableAttributes as $attribute) { + foreach ($this->removableAttributes['*'] as $attribute) { if (!isset($requestedAttributes[$attribute])) { $document->removeAttribute($attribute); } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index ff25e799c1..a36c563747 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -212,7 +212,6 @@ class Migrations extends Action // set the errors back without trace $clonedMigrationDocument->setAttribute('errors', $errorMessages); - /** Trigger Realtime Events */ $queueForRealtime ->setProject($project) diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 0df0a2f5af..fbd0714d26 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -2238,8 +2238,8 @@ trait DatabasesBase $update['body']['$sequence'] = 123; $update['body']['$databaseId'] = 'random'; $update['body']['$collectionId'] = 'random'; - $update['body']['$createdAt'] = '2024-01-01T00:00:00Z'; - $update['body']['$updatedAt'] = '2024-01-01T00:00:00Z'; + $update['body']['$createdAt'] = '2024-01-01T00:00:00.000+00:00'; + $update['body']['$updatedAt'] = '2024-01-01T00:00:00.000+00:00'; $upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([ 'content-type' => 'application/json', @@ -2253,8 +2253,14 @@ trait DatabasesBase $this->assertEquals($personNoPerm['body']['$collectionId'], $upserted['body']['$collectionId']); $this->assertEquals($personNoPerm['body']['$databaseId'], $upserted['body']['$databaseId']); $this->assertEquals($personNoPerm['body']['$sequence'], $upserted['body']['$sequence']); - $this->assertEquals($personNoPerm['body']['$createdAt'], $upserted['body']['$createdAt']); - $this->assertNotEquals('2024-01-01T00:00:00Z', $upserted['body']['$updatedAt']); + + if ($this->getSide() === 'client') { + $this->assertEquals($personNoPerm['body']['$createdAt'], $upserted['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$updatedAt']); + } else { + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$createdAt']); + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$updatedAt']); + } } } @@ -3055,8 +3061,8 @@ trait DatabasesBase '$sequence' => 9999, '$collectionId' => 'newCollectionId', '$databaseId' => 'newDatabaseId', - '$createdAt' => '2024-01-01T00:00:00+00:00', - '$updatedAt' => '2024-01-01T00:00:00+00:00', + '$createdAt' => '2024-01-01T00:00:00.000+00:00', + '$updatedAt' => '2024-01-01T00:00:00.000+00:00', 'title' => 'Thor: Ragnarok', ], ]); @@ -3065,10 +3071,16 @@ trait DatabasesBase $this->assertEquals($id, $response['body']['$id']); $this->assertEquals($data['moviesId'], $response['body']['$collectionId']); $this->assertEquals($databaseId, $response['body']['$databaseId']); - $this->assertNotEquals('2024-01-01T00:00:00+00:00', $response['body']['$createdAt']); - $this->assertNotEquals('2024-01-01T00:00:00+00:00', $response['body']['$updatedAt']); $this->assertNotEquals(9999, $response['body']['$sequence']); + if ($this->getSide() === 'client') { + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']); + } else { + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']); + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']); + } + return []; } From 8834163c91378cd46592f30207e22068daec2c52 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 21:01:09 +1200 Subject: [PATCH 04/22] Fix tests --- .../Databases/Legacy/DatabasesBase.php | 4 +- .../Legacy/DatabasesCustomClientTest.php | 153 ----------------- .../Databases/TablesDB/DatabasesBase.php | 5 +- .../TablesDB/DatabasesCustomClientTest.php | 154 ------------------ 4 files changed, 6 insertions(+), 310 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index fbd0714d26..59864255ab 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -4341,7 +4341,9 @@ trait DatabasesBase ] ]); if ($this->getSide() === 'client') { - $this->assertEquals($document['headers']['status-code'], 400); + $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); + $this->assertNotEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); + $this->assertNotEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); } else { $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); $this->assertEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php index 23153e8f39..699a2b8f25 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php @@ -889,157 +889,4 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - public function testModifyCreatedAtUpdatedAtSingleDocument(): void - { - $database = $this->client->call(Client::METHOD_POST, '/databases', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Test Database' - ]); - - $databaseId = $database['body']['$id']; - - $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Test Table', - 'documentsecurity' => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ], - ]); - $collectionId = $table['body']['$id']; - - // Create string column - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - - sleep(1); - - // Test 1: Try to create document with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 2: Try to create document with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 3: Try to create document with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 4: Create a valid document first - $validRow = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Valid Movie' - ] - ]); - - $this->assertEquals(201, $validRow['headers']['status-code']); - $documentId = $validRow['body']['$id']; - - // Test 5: Try to update document with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 6: Try to update document with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 7: Try to update document with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Cleanup - $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - } } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index c57421c384..c2a1ee859d 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -4269,9 +4269,10 @@ trait DatabasesBase ]); if ($this->getSide() === 'client') { - $this->assertEquals($row['headers']['status-code'], 400); - } else { $this->assertEquals($row['body']['title'], 'Again Updated Date Test'); + $this->assertNotEquals($row['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); + $this->assertNotEquals($row['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); + } else { $this->assertEquals($row['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); $this->assertEquals($row['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php index 277771e2df..f986b5dd03 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php @@ -890,158 +890,4 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - - public function testModifyCreatedAtUpdatedAtSingleRow(): void - { - $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Test Database' - ]); - - $databaseId = $database['body']['$id']; - - $table = $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' => 'Test Table', - 'rowSecurity' => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ], - ]); - $tableId = $table['body']['$id']; - - // Create string column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - - sleep(1); - - // Test 1: Try to create row with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 2: Try to create row with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 3: Try to create row with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 4: Create a valid row first - $validRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Valid Movie' - ] - ]); - - $this->assertEquals(201, $validRow['headers']['status-code']); - $rowId = $validRow['body']['$id']; - - // Test 5: Try to update row with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 6: Try to update row with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 7: Try to update row with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Cleanup - $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - } } From 1e230a93a9886d7811fd5dcc97fd95089782454a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 21:06:13 +1200 Subject: [PATCH 05/22] Format --- .../Databases/Http/Databases/Collections/Documents/Action.php | 3 +-- 1 file changed, 1 insertion(+), 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 c952b3bc6e..d1d0738990 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 @@ -212,8 +212,7 @@ abstract class Action extends AppwriteAction protected function removeReadonlyAttributes( Document|array $document, bool $privileged = false, - ): Document|array - { + ): Document|array { foreach ($this->removableAttributes['*'] as $attribute) { unset($document[$attribute]); } From eab03af8223956a1c807b7a2d89521c817f67830 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 29 Aug 2025 22:45:02 +1200 Subject: [PATCH 06/22] Revert "Added handling of database resources after migration" --- composer.lock | 12 +- src/Appwrite/Event/StatsUsage.php | 7 -- src/Appwrite/Platform/Workers/Migrations.php | 110 +------------------ 3 files changed, 10 insertions(+), 119 deletions(-) diff --git a/composer.lock b/composer.lock index 1a776c089e..3bf17bc228 100644 --- a/composer.lock +++ b/composer.lock @@ -4109,16 +4109,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" + "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", + "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", "shasum": "" }, "require": { @@ -4159,9 +4159,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.1" + "source": "https://github.com/utopia-php/migration/tree/1.0.0" }, - "time": "2025-08-28T13:41:25+00:00" + "time": "2025-08-13T09:15:53+00:00" }, { "name": "utopia-php/orchestration", diff --git a/src/Appwrite/Event/StatsUsage.php b/src/Appwrite/Event/StatsUsage.php index f9e03c7c3d..f6b1d695f4 100644 --- a/src/Appwrite/Event/StatsUsage.php +++ b/src/Appwrite/Event/StatsUsage.php @@ -85,11 +85,4 @@ class StatsUsage extends Event }), ]; } - - public function reset(): Event - { - $this->metrics = []; - parent::reset(); - return $this; - } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index a36c563747..cd7a6a1058 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Realtime; -use Appwrite\Event\StatsUsage; use Exception; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -14,14 +13,9 @@ use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; -use Utopia\Database\Validator\Authorization as AuthorizationValidator; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Exception as MigrationException; -use Utopia\Migration\Resource; -use Utopia\Migration\Resources\Database\Database as ResourceDatabase; -use Utopia\Migration\Resources\Database\Row as ResourceRow; -use Utopia\Migration\Resources\Database\Table as ResourceTable; use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Sources\CSV; @@ -51,7 +45,6 @@ class Migrations extends Action */ protected array $sourceReport = []; - private string $source; /** * @var callable */ @@ -76,14 +69,13 @@ class Migrations extends Action ->inject('logError') ->inject('queueForRealtime') ->inject('deviceForImports') - ->inject('queueForStatsUsage') ->callback($this->action(...)); } /** * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports, StatsUsage $queueForStatsUsage): void + public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports): void { $payload = $message->getPayload() ?? []; $this->deviceForImports = $deviceForImports; @@ -111,7 +103,7 @@ class Migrations extends Action return; } - $this->processMigration($migration, $queueForRealtime, $queueForStatsUsage); + $this->processMigration($migration, $queueForRealtime); } /** @@ -274,7 +266,7 @@ class Migrations extends Action * @throws \Utopia\Database\Exception * @throws Exception */ - protected function processMigration(Document $migration, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage): void + protected function processMigration(Document $migration, Realtime $queueForRealtime): void { $project = $this->project; $projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId()); @@ -308,7 +300,6 @@ class Migrations extends Action $destination ); - $aggregatedResources = []; /** Start Transfer */ if (empty($source->getErrors())) { $migration->setAttribute('stage', 'migrating'); @@ -316,40 +307,9 @@ class Migrations extends Action $transfer->run( $migration->getAttribute('resources'), - function ($resources) use ($migration, $transfer, $projectDocument, $queueForRealtime, &$aggregatedResources) { + function () use ($migration, $transfer, $projectDocument, $queueForRealtime) { $migration->setAttribute('resourceData', json_encode($transfer->getCache())); $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - - if (!empty($resources)) { - /** - * @var Resource $resource - */ - $resource = $resources[0]; - $count = count($resources); - $databaseId = null; - $tableId = null; - switch ($resource->getName()) { - case ResourceTable::getName(): - /** @var ResourceTable $resource */ - $databaseId = $resource->getDatabase()->getSequence(); - break; - case ResourceRow::getName(): - /** @var ResourceRow $resource */ - $table = $resource->getTable(); - $databaseId = $table->getDatabase()->getSequence(); - $tableId = $table->getSequence(); - break; - default: - break; - } - $aggregatedResources[] = [ - 'name' => $resource->getName(), - 'count' => $count, - 'databaseId' => $databaseId, - 'tableId' => $tableId - ]; - - } $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); }, $migration->getAttribute('resourceId'), @@ -451,71 +411,9 @@ class Migrations extends Action } if ($migration->getAttribute('status', '') === 'completed') { - foreach ($aggregatedResources as $resource) { - $this->processMigrationResourceStats( - $resource, - $queueForStatsUsage, - $projectDocument, - $migration->getAttribute('source'), - $migration->getAttribute('resourceId') - ); - } $destination?->success(); $source?->success(); } } } - - private function processMigrationResourceStats(array $resources, StatsUsage $queueForStatsUsage, Document $projectDocument, string $source, ?string $resourceId) - { - $resourceName = $resources['name']; - $count = $resources['count']; - $databaseInternalId = $resources['databaseId']; - $tableInternalId = $resources['tableId']; - - if ($source === CSV::getName()) { - [$databaseId, $tableId] = explode(':', $resourceId); - $database = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('databases', $databaseId)); - $table = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('database_' . $database->getSequence(), $tableId)); - $databaseInternalId = (int) $database->getSequence(); - $tableInternalId = (int) $table->getSequence(); - } - - switch ($resourceName) { - case ResourceDatabase::getName(): - $queueForStatsUsage->addMetric(METRIC_DATABASES, $count); - break; - - case ResourceTable::getName(): - $queueForStatsUsage - ->addMetric(METRIC_COLLECTIONS, $count) - ->addMetric( - str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), - $count - ); - break; - - case ResourceRow::getName(): - $queueForStatsUsage - ->addMetric( - str_replace( - ['{databaseInternalId}','{collectionInternalId}'], - [$databaseInternalId, $tableInternalId], - METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS - ), - $count - ) - ->addMetric( - str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), - $count - ); - break; - - default: - break; - } - - $queueForStatsUsage->setProject($projectDocument)->trigger(); - $queueForStatsUsage->reset(); - } } From 262be06b0d693f8a8d33c0e001255edc803bad7f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 29 Aug 2025 18:37:44 +0530 Subject: [PATCH 07/22] chore: improve deprecation warning --- app/controllers/general.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 22954afd96..91ba1477bd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1065,7 +1065,6 @@ App::init() */ /** @var \Appwrite\SDK\Method $sdk */ $sdk = $route->getLabel('sdk', false); - $deprecationWarning = 'This route is deprecated. See the updated documentation for improved compatibility and migration details.'; $sdkItems = is_array($sdk) ? $sdk : (!empty($sdk) ? [$sdk] : []); if (!empty($sdkItems) && count($sdkItems) > 0) { $allDeprecated = true; @@ -1076,6 +1075,13 @@ App::init() } } if ($allDeprecated) { + $deprecatedMethod = $sdkItems[0]->getDeprecated(); + $replaceWith = $deprecatedMethod->getReplaceWith(); + if ($replaceWith) { + $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); + } + $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/server-rest/' . $replaceWith; + $deprecationWarning = 'Route ' . $route->getPath() . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use ' . $deprecatedMethod->getReplaceWith() . ' instead. See: ' . $deprecatedReplaceWithLink; $warnings[] = $deprecationWarning; } } From 4ef6c23a67009407b4a664cdb7537319ffc04a8c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 29 Aug 2025 18:41:46 +0530 Subject: [PATCH 08/22] add quotes --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 91ba1477bd..2138bad97b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1081,7 +1081,7 @@ App::init() $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/server-rest/' . $replaceWith; - $deprecationWarning = 'Route ' . $route->getPath() . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use ' . $deprecatedMethod->getReplaceWith() . ' instead. See: ' . $deprecatedReplaceWithLink; + $deprecationWarning = 'Route ' . $route->getPath() . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; $warnings[] = $deprecationWarning; } } From 674d928ce24826d4bef3edc7f4e4e186fc907ac0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 29 Aug 2025 21:33:53 +0530 Subject: [PATCH 09/22] show method is x-sdk-name is present --- app/controllers/general.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 2138bad97b..4561c78554 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1081,7 +1081,8 @@ App::init() $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/server-rest/' . $replaceWith; - $deprecationWarning = 'Route ' . $route->getPath() . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + $isSdkUsed = !empty($request->getHeader('x-sdk-name')) || !empty($request->getHeader('x-sdk-version')); + $deprecationWarning = (!$isSdkUsed ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; $warnings[] = $deprecationWarning; } } From 73f5823746d9e9e747a6aa399cb5fb17c5aa3376 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 30 Aug 2025 11:27:51 +0530 Subject: [PATCH 10/22] chore: mark Row response model's param with readonly --- app/config/specs/open-api3-1.8.x-client.json | 9 ++++++--- app/config/specs/open-api3-1.8.x-console.json | 9 ++++++--- app/config/specs/open-api3-1.8.x-server.json | 9 ++++++--- app/config/specs/open-api3-latest-client.json | 9 ++++++--- app/config/specs/open-api3-latest-console.json | 9 ++++++--- app/config/specs/open-api3-latest-server.json | 9 ++++++--- app/config/specs/swagger2-1.8.x-client.json | 9 ++++++--- app/config/specs/swagger2-1.8.x-console.json | 9 ++++++--- app/config/specs/swagger2-1.8.x-server.json | 9 ++++++--- app/config/specs/swagger2-latest-client.json | 9 ++++++--- app/config/specs/swagger2-latest-console.json | 9 ++++++--- app/config/specs/swagger2-latest-server.json | 9 ++++++--- src/Appwrite/Utopia/Response/Model/Row.php | 3 +++ 13 files changed, 75 insertions(+), 36 deletions(-) diff --git a/app/config/specs/open-api3-1.8.x-client.json b/app/config/specs/open-api3-1.8.x-client.json index e3fd3bd04a..0841dabbd1 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -9948,17 +9948,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index 29c45692ec..31cb8e5105 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -47120,17 +47120,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index 60c07da12a..0fe1cf72ae 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -35978,17 +35978,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index e3fd3bd04a..0841dabbd1 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -9948,17 +9948,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 29c45692ec..31cb8e5105 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -47120,17 +47120,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 60c07da12a..0fe1cf72ae 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -35978,17 +35978,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index 3a6152b6ad..efeb167a24 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -9944,17 +9944,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 5e121e8840..788279b595 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -47166,17 +47166,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index e4ffbbe973..9293ce6928 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -36115,17 +36115,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 3a6152b6ad..efeb167a24 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -9944,17 +9944,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 5e121e8840..788279b595 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -47166,17 +47166,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index e4ffbbe973..9293ce6928 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -36115,17 +36115,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/src/Appwrite/Utopia/Response/Model/Row.php b/src/Appwrite/Utopia/Response/Model/Row.php index 370d3065ba..14a9ec9cda 100644 --- a/src/Appwrite/Utopia/Response/Model/Row.php +++ b/src/Appwrite/Utopia/Response/Model/Row.php @@ -41,18 +41,21 @@ class Row extends Any 'description' => 'Row automatically incrementing ID.', 'default' => 0, 'example' => 1, + 'readOnly' => true, ]) ->addRule('$tableId', [ 'type' => self::TYPE_STRING, 'description' => 'Table ID.', 'default' => '', 'example' => '5e5ea5c15117e', + 'readOnly' => true, ]) ->addRule('$databaseId', [ 'type' => self::TYPE_STRING, 'description' => 'Database ID.', 'default' => '', 'example' => '5e5ea5c15117e', + 'readOnly' => true, ]) ->addRule('$createdAt', [ 'type' => self::TYPE_DATETIME, From 2710282216001a239f9fc2d9b0590f272fa8bfe5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 30 Aug 2025 12:25:47 +0530 Subject: [PATCH 11/22] chore: use correct sdk name and platform in link --- app/controllers/general.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 4561c78554..57e1728a1b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1076,13 +1076,16 @@ App::init() } if ($allDeprecated) { $deprecatedMethod = $sdkItems[0]->getDeprecated(); + $replaceWith = $deprecatedMethod->getReplaceWith(); if ($replaceWith) { $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } - $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/server-rest/' . $replaceWith; - $isSdkUsed = !empty($request->getHeader('x-sdk-name')) || !empty($request->getHeader('x-sdk-version')); - $deprecationWarning = (!$isSdkUsed ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + $sdkName = $request->getHeader('x-sdk-name', 'rest'); + $sdkPlatform = $request->getHeader('x-sdk-platform', 'server'); + $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . strtolower($sdkName) . '/' . $replaceWith; + + $deprecationWarning = (!empty($sdkName) ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; $warnings[] = $deprecationWarning; } } From 4ee0c12cb16685c065caf6c9c6cf9f41de3826e4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 30 Aug 2025 12:27:50 +0530 Subject: [PATCH 12/22] chore: fix --- app/controllers/general.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 57e1728a1b..f1f6c52354 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1082,10 +1082,10 @@ App::init() $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } $sdkName = $request->getHeader('x-sdk-name', 'rest'); - $sdkPlatform = $request->getHeader('x-sdk-platform', 'server'); + $sdkPlatform = !empty($sdkName) ? $request->getHeader('x-sdk-platform', 'server') : 'server'; $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . strtolower($sdkName) . '/' . $replaceWith; - $deprecationWarning = (!empty($sdkName) ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + $deprecationWarning = (empty($sdkName) ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; $warnings[] = $deprecationWarning; } } From c6fb4687a29545d4bffee65f8e8eb87de4a684ea Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 30 Aug 2025 12:32:57 +0530 Subject: [PATCH 13/22] readability --- app/controllers/general.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index f1f6c52354..a38e9c9591 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1081,11 +1081,22 @@ App::init() if ($replaceWith) { $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } - $sdkName = $request->getHeader('x-sdk-name', 'rest'); - $sdkPlatform = !empty($sdkName) ? $request->getHeader('x-sdk-platform', 'server') : 'server'; + + $sdkNameHeader = $request->getHeader('x-sdk-name', ''); + $sdkPlatformHeader = $request->getHeader('x-sdk-platform', ''); + + $sdkExists = !empty($sdkNameHeader); + $sdkName = $sdkExists ? $sdkNameHeader : 'rest'; + $sdkPlatform = !empty($sdkPlatformHeader) ? $sdkPlatformHeader : 'server'; + $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . strtolower($sdkName) . '/' . $replaceWith; - $deprecationWarning = (empty($sdkName) ? 'Route ' . $route->getPath() : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`') . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + $deprecationWarning = ( + !$sdkExists + ? 'Route ' . $route->getPath() + : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`' + ) . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + $warnings[] = $deprecationWarning; } } From 16d731b748a63ca196aab6325b65b1148fa938f2 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 30 Aug 2025 12:34:27 +0530 Subject: [PATCH 14/22] lower --- app/controllers/general.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index a38e9c9591..dfa682d33d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1082,14 +1082,14 @@ App::init() $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); } - $sdkNameHeader = $request->getHeader('x-sdk-name', ''); - $sdkPlatformHeader = $request->getHeader('x-sdk-platform', ''); + $sdkNameHeader = strtolower($request->getHeader('x-sdk-name', '')); + $sdkPlatformHeader = strtolower($request->getHeader('x-sdk-platform', '')); $sdkExists = !empty($sdkNameHeader); $sdkName = $sdkExists ? $sdkNameHeader : 'rest'; $sdkPlatform = !empty($sdkPlatformHeader) ? $sdkPlatformHeader : 'server'; - $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . strtolower($sdkName) . '/' . $replaceWith; + $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . $sdkName . '/' . $replaceWith; $deprecationWarning = ( !$sdkExists From bc18f9d8c7e9e46c3a1624e1e49b258f6c60d0fe Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 1 Sep 2025 12:29:08 +0530 Subject: [PATCH 15/22] temporary remove warnings --- app/controllers/general.php | 60 ++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index dfa682d33d..4f97183dfe 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1064,42 +1064,42 @@ App::init() * Deprecation Warning */ /** @var \Appwrite\SDK\Method $sdk */ - $sdk = $route->getLabel('sdk', false); - $sdkItems = is_array($sdk) ? $sdk : (!empty($sdk) ? [$sdk] : []); - if (!empty($sdkItems) && count($sdkItems) > 0) { - $allDeprecated = true; - foreach ($sdkItems as $sdkItem) { - if (!$sdkItem->isDeprecated()) { - $allDeprecated = false; - break; - } - } - if ($allDeprecated) { - $deprecatedMethod = $sdkItems[0]->getDeprecated(); + // $sdk = $route->getLabel('sdk', false); + // $sdkItems = is_array($sdk) ? $sdk : (!empty($sdk) ? [$sdk] : []); + // if (!empty($sdkItems) && count($sdkItems) > 0) { + // $allDeprecated = true; + // foreach ($sdkItems as $sdkItem) { + // if (!$sdkItem->isDeprecated()) { + // $allDeprecated = false; + // break; + // } + // } + // if ($allDeprecated) { + // $deprecatedMethod = $sdkItems[0]->getDeprecated(); - $replaceWith = $deprecatedMethod->getReplaceWith(); - if ($replaceWith) { - $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); - } + // $replaceWith = $deprecatedMethod->getReplaceWith(); + // if ($replaceWith) { + // $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); + // } - $sdkNameHeader = strtolower($request->getHeader('x-sdk-name', '')); - $sdkPlatformHeader = strtolower($request->getHeader('x-sdk-platform', '')); + // $sdkNameHeader = strtolower($request->getHeader('x-sdk-name', '')); + // $sdkPlatformHeader = strtolower($request->getHeader('x-sdk-platform', '')); - $sdkExists = !empty($sdkNameHeader); - $sdkName = $sdkExists ? $sdkNameHeader : 'rest'; - $sdkPlatform = !empty($sdkPlatformHeader) ? $sdkPlatformHeader : 'server'; + // $sdkExists = !empty($sdkNameHeader); + // $sdkName = $sdkExists ? $sdkNameHeader : 'rest'; + // $sdkPlatform = !empty($sdkPlatformHeader) ? $sdkPlatformHeader : 'server'; - $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . $sdkName . '/' . $replaceWith; + // $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . $sdkName . '/' . $replaceWith; - $deprecationWarning = ( - !$sdkExists - ? 'Route ' . $route->getPath() - : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`' - ) . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; + // $deprecationWarning = ( + // !$sdkExists + // ? 'Route ' . $route->getPath() + // : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`' + // ) . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; - $warnings[] = $deprecationWarning; - } - } + // $warnings[] = $deprecationWarning; + // } + // } if (!empty($warnings)) { $response->addHeader('X-Appwrite-Warning', implode(';', $warnings)); From 7600edb102d51ca7e2856e5ac29bea66acff5b45 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 1 Sep 2025 13:03:41 +0530 Subject: [PATCH 16/22] remove --- app/controllers/general.php | 41 ------------------------------------- 1 file changed, 41 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 4f97183dfe..40ce66b574 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1060,47 +1060,6 @@ App::init() $response->addHeader('Access-Control-Allow-Origin', '*'); } - /** - * Deprecation Warning - */ - /** @var \Appwrite\SDK\Method $sdk */ - // $sdk = $route->getLabel('sdk', false); - // $sdkItems = is_array($sdk) ? $sdk : (!empty($sdk) ? [$sdk] : []); - // if (!empty($sdkItems) && count($sdkItems) > 0) { - // $allDeprecated = true; - // foreach ($sdkItems as $sdkItem) { - // if (!$sdkItem->isDeprecated()) { - // $allDeprecated = false; - // break; - // } - // } - // if ($allDeprecated) { - // $deprecatedMethod = $sdkItems[0]->getDeprecated(); - - // $replaceWith = $deprecatedMethod->getReplaceWith(); - // if ($replaceWith) { - // $replaceWith = preg_replace('/\./', '#', $replaceWith, 1); - // } - - // $sdkNameHeader = strtolower($request->getHeader('x-sdk-name', '')); - // $sdkPlatformHeader = strtolower($request->getHeader('x-sdk-platform', '')); - - // $sdkExists = !empty($sdkNameHeader); - // $sdkName = $sdkExists ? $sdkNameHeader : 'rest'; - // $sdkPlatform = !empty($sdkPlatformHeader) ? $sdkPlatformHeader : 'server'; - - // $deprecatedReplaceWithLink = 'https://appwrite.io/docs/references/cloud/' . $sdkPlatform . '-' . $sdkName . '/' . $replaceWith; - - // $deprecationWarning = ( - // !$sdkExists - // ? 'Route ' . $route->getPath() - // : 'Method `' . $sdkItems[0]->getNamespace() . '.' . $sdkItems[0]->getMethodName() . '`' - // ) . ' is deprecated since ' . $deprecatedMethod->getSince() . '. Please use `' . $deprecatedMethod->getReplaceWith() . '` instead. See: ' . $deprecatedReplaceWithLink; - - // $warnings[] = $deprecationWarning; - // } - // } - if (!empty($warnings)) { $response->addHeader('X-Appwrite-Warning', implode(';', $warnings)); } From a20f71c511becb2132b499de69af1b982c6dc721 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 1 Sep 2025 13:08:17 +0530 Subject: [PATCH 17/22] chore: update exception thrown when svg sanitization fails --- app/config/errors.php | 5 +++++ app/controllers/api/avatars.php | 2 +- src/Appwrite/Extend/Exception.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/config/errors.php b/app/config/errors.php index 23df60f4ba..c4617f1cde 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -435,6 +435,11 @@ return [ 'description' => 'The requested favicon could not be found.', 'code' => 404, ], + Exception::AVATAR_SVG_SANITIZATION_FAILED => [ + 'name' => Exception::AVATAR_SVG_SANITIZATION_FAILED, + 'description' => 'SVG sanitization failed.', + 'code' => 400, + ], /** Storage */ Exception::STORAGE_FILE_ALREADY_EXISTS => [ diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 785324739b..90364d997e 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -474,7 +474,7 @@ App::get('/v1/avatars/favicon') $sanitizer->minify(true); $cleanSvg = $sanitizer->sanitize($data); if ($cleanSvg === false) { - throw new \Exception('SVG sanitization failed'); + throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED); } $response ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 8eded2dbe0..9849352e56 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -134,6 +134,7 @@ class Exception extends \Exception public const AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found'; public const AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed'; public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found'; + public const AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed'; /** Storage */ public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists'; From 97366e7fbd4de055e833e5bd9758d259165c6ef6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 1 Sep 2025 19:39:13 +1200 Subject: [PATCH 18/22] Allow null for targets/topics/users --- app/controllers/api/messaging.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index abc1f9bc52..d22c5cb2c2 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3011,7 +3011,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3184,7 +3184,7 @@ App::post('/v1/messaging/messages/sms') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3319,7 +3319,7 @@ App::post('/v1/messaging/messages/push') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; From 364eacc3e2784a3a7cee8f5daa6928b92b461682 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 1 Sep 2025 20:17:49 +1200 Subject: [PATCH 19/22] Remove dump --- .../Databases/Http/Databases/Collections/Documents/Update.php | 2 -- 1 file changed, 2 deletions(-) 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 556d14219f..8382bdd5e9 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 @@ -231,8 +231,6 @@ class Update extends Action ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations); - \var_dump($newDocument); - try { $document = $dbForProject->withRequestTimestamp( $requestTimestamp, From fb8270733e59d09b0aaf881ef5cc0f4dd1758c01 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 1 Sep 2025 22:09:30 +1200 Subject: [PATCH 20/22] Allow running tests with specific response format --- .github/workflows/tests.yml | 28 ++++++++++++++++++++++++---- tests/e2e/Client.php | 14 ++++++++++++++ tests/e2e/Scopes/Scope.php | 6 ++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 164599f911..cebdc02163 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,15 @@ env: IMAGE: appwrite-dev CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }} -on: [ pull_request ] +on: + pull_request: + workflow_dispatch: + inputs: + response_format: + description: 'Response format version to test (e.g., 1.5.0, 1.4.0)' + required: false + type: string + default: '' jobs: check_database_changes: @@ -100,7 +108,10 @@ jobs: run: docker compose exec -T appwrite vars - name: Run Unit Tests - run: docker compose exec appwrite test /usr/src/code/tests/unit + run: | + docker compose exec \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ + appwrite test /usr/src/code/tests/unit e2e_general_test: name: E2E General Test @@ -132,7 +143,10 @@ jobs: done - name: Run General Tests - run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug + run: | + docker compose exec -T \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ + appwrite test /usr/src/code/tests/e2e/General --debug - name: Failure Logs if: failure() @@ -208,6 +222,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots - name: Failure Logs @@ -296,6 +311,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots - name: Failure Logs @@ -337,6 +353,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys - name: Failure Logs @@ -392,6 +409,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys - name: Failure Logs @@ -434,6 +452,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots - name: Failure Logs @@ -490,6 +509,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots - name: Failure Logs @@ -498,4 +518,4 @@ jobs: echo "=== Appwrite Worker Builds Logs ===" docker compose logs appwrite-worker-builds echo "=== OpenRuntimes Executor Logs ===" - docker compose logs openruntimes-executor + docker compose logs openruntimes-executor \ No newline at end of file diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index e411b68454..6b81713654 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -108,6 +108,20 @@ class Client return $this; } + /** + * Set Response Format + * + * @param string $value + * + * @return self $this + */ + public function setResponseFormat(string $value): self + { + $this->addHeader('X-Appwrite-Response-Format', $value); + + return $this; + } + /** * @param bool $status true * @return self $this diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 2dbeae961e..c967ab8bb4 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -7,6 +7,7 @@ use Appwrite\Tests\Retryable; use PHPUnit\Framework\TestCase; use Tests\E2E\Client; use Utopia\Database\Helpers\ID; +use Utopia\System\System; abstract class Scope extends TestCase { @@ -23,6 +24,11 @@ abstract class Scope extends TestCase { $this->client = new Client(); $this->client->setEndpoint($this->endpoint); + + $format = System::getEnv('_APP_E2E_RESPONSE_FORMAT'); + if (!empty($format)) { + $this->client->setResponseFormat($format); + } } protected function tearDown(): void From 6e5fe9c33ff61a008635771a15182e23366f1fe9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 1 Sep 2025 22:41:30 +1200 Subject: [PATCH 21/22] Check verison --- tests/e2e/Scopes/Scope.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index c967ab8bb4..5b7f1a8771 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -27,6 +27,12 @@ abstract class Scope extends TestCase $format = System::getEnv('_APP_E2E_RESPONSE_FORMAT'); if (!empty($format)) { + if ( + !\preg_match('/^\d+\.\d+\.\d+$/', $format) || + !\version_compare($format, APP_VERSION_STABLE, '<=') + ) { + throw new \Exception('E2E response format must be ' . APP_VERSION_STABLE . ' or lower.'); + } $this->client->setResponseFormat($format); } } From 48c54bf5e82e5a9556ffaed8930e761377734ff7 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:46:38 +0100 Subject: [PATCH 22/22] chore: make webhooks publisher overridable --- app/controllers/shared/api.php | 6 +++--- app/init/resources.php | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7742aac18f..6dcb99b56f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -29,7 +29,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Publisher; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -416,6 +415,7 @@ App::init() ->inject('user') ->inject('publisher') ->inject('publisherFunctions') + ->inject('publisherWebhooks') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('queueForAudits') @@ -431,7 +431,7 @@ App::init() ->inject('plan') ->inject('devKey') ->inject('telemetry') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, BrokerPool $publisherFunctions, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) { $route = $utopia->getRoute(); @@ -544,7 +544,7 @@ App::init() // from overwriting the events that are supposed to be triggered in the shutdown hook. $queueForEventsClone = new Event($publisher); $queueForFunctions = new Func($publisherFunctions); - $queueForWebhooks = new Webhook($publisher); + $queueForWebhooks = new Webhook($publisherWebhooks); $queueForRealtime = new Realtime(); $dbForProject diff --git a/app/init/resources.php b/app/init/resources.php index 380087cf43..d4f0433447 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -84,25 +84,28 @@ App::setResource('localeCodes', function () { App::setResource('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -App::setResource('publisherDatabases', function (BrokerPool $publisher) { +App::setResource('publisherDatabases', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherFunctions', function (BrokerPool $publisher) { +App::setResource('publisherFunctions', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMigrations', function (BrokerPool $publisher) { +App::setResource('publisherMigrations', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherStatsUsage', function (BrokerPool $publisher) { +App::setResource('publisherStatsUsage', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMails', function (BrokerPool $publisher) { +App::setResource('publisherMails', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherDeletes', function (BrokerPool $publisher) { +App::setResource('publisherDeletes', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMessaging', function (BrokerPool $publisher) { +App::setResource('publisherMessaging', function (Publisher $publisher) { + return $publisher; +}, ['publisher']); +App::setResource('publisherWebhooks', function (Publisher $publisher) { return $publisher; }, ['publisher']); App::setResource('queueForMessaging', function (Publisher $publisher) {