From de69692ff2ee52a7e3f676bf4eea49ac09d77654 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 20:32:14 +0530 Subject: [PATCH 1/6] added modify createdAt/updatedAt support --- composer.json | 2 +- composer.lock | 122 ++- .../Collections/Documents/Bulk/Update.php | 22 +- .../Collections/Documents/Bulk/Upsert.php | 20 +- .../Collections/Documents/Create.php | 24 +- .../Collections/Documents/Update.php | 16 +- .../Collections/Documents/Upsert.php | 28 +- .../Databases/Legacy/DatabasesBase.php | 18 +- .../Legacy/DatabasesCustomClientTest.php | 153 +++ .../Legacy/DatabasesCustomServerTest.php | 932 ++++++++++++++++++ 10 files changed, 1246 insertions(+), 91 deletions(-) diff --git a/composer.json b/composer.json index 659d6221f7..08329aaa05 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.73.0", + "utopia-php/database": "0.74.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dsn": "0.2.1", diff --git a/composer.lock b/composer.lock index 718dbaf64b..d3b9c42015 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4876940be4499bea26a77a036dece4f", + "content-hash": "43a92af943693f80341a8582cf582986", "packages": [ { "name": "adhocore/jwt", @@ -2547,16 +2547,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" + "reference": "1c064a0c67749923483216b081066642751cc2c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", + "reference": "1c064a0c67749923483216b081066642751cc2c7", "shasum": "" }, "require": { @@ -2622,7 +2622,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.1" + "source": "https://github.com/symfony/http-client/tree/v7.3.2" }, "funding": [ { @@ -2633,12 +2633,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T07:58:39+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/http-client-contracts", @@ -3493,16 +3497,16 @@ }, { "name": "utopia-php/database", - "version": "0.73.0", + "version": "0.74.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "f9b6c587e54069dc64c2d29174ce8a42b7ba6491" + "reference": "065b4812799d57fd2c596f88aadd51644247c7e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/f9b6c587e54069dc64c2d29174ce8a42b7ba6491", - "reference": "f9b6c587e54069dc64c2d29174ce8a42b7ba6491", + "url": "https://api.github.com/repos/utopia-php/database/zipball/065b4812799d57fd2c596f88aadd51644247c7e3", + "reference": "065b4812799d57fd2c596f88aadd51644247c7e3", "shasum": "" }, "require": { @@ -3543,9 +3547,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.73.0" + "source": "https://github.com/utopia-php/database/tree/0.74.3" }, - "time": "2025-07-27T04:10:06+00:00" + "time": "2025-07-31T15:00:30+00:00" }, { "name": "utopia-php/detector", @@ -4810,16 +4814,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.41.24", + "version": "0.41.26", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "c9ebf8daa25332ca701515d42e8d0c4a7be6a489" + "reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c9ebf8daa25332ca701515d42e8d0c4a7be6a489", - "reference": "c9ebf8daa25332ca701515d42e8d0c4a7be6a489", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b", + "reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b", "shasum": "" }, "require": { @@ -4855,9 +4859,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.41.24" + "source": "https://github.com/appwrite/sdk-generator/tree/0.41.26" }, - "time": "2025-07-28T06:27:00+00:00" + "time": "2025-07-30T06:53:12+00:00" }, { "name": "doctrine/annotations", @@ -7258,16 +7262,16 @@ }, { "name": "symfony/console", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "shasum": "" }, "require": { @@ -7332,7 +7336,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.1" + "source": "https://github.com/symfony/console/tree/v7.3.2" }, "funding": [ { @@ -7343,25 +7347,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-30T17:13:41+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", "shasum": "" }, "require": { @@ -7398,7 +7406,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" }, "funding": [ { @@ -7409,25 +7417,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-07-07T08:17:47+00:00" }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", "shasum": "" }, "require": { @@ -7462,7 +7474,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.2" }, "funding": [ { @@ -7473,25 +7485,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", "shasum": "" }, "require": { @@ -7529,7 +7545,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" }, "funding": [ { @@ -7540,12 +7556,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T13:12:05+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/polyfill-ctype", @@ -7924,16 +7944,16 @@ }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { @@ -7991,7 +8011,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -8002,12 +8022,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "textalk/websocket", 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 daa7179395..14bd71c5ea 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 @@ -125,16 +125,18 @@ class Update extends Action $documents = []; try { - $modified = $dbForProject->updateDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - new Document($data), - $queries, - onNext: function (Document $document) use ($plan, &$documents) { - if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $documents[] = $document; - } - }, - ); + $modified = $dbForProject->withPreserveDates(function () use ($plan, &$documents, $dbForProject, $database, $collection, $data, $queries) { + return $dbForProject->updateDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + new Document($data), + $queries, + onNext: function (Document $document) use ($plan, &$documents) { + if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $documents[] = $document; + } + }, + ); + }); } catch (ConflictException) { throw new Exception($this->getConflictException()); } catch (RelationshipException $e) { 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 6b5da25a2c..23e6453138 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 @@ -106,15 +106,17 @@ class Upsert extends Action $upserted = []; try { - $modified = $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documents, - onNext: function (Document $document) use ($plan, &$upserted) { - if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $upserted[] = $document; - } - }, - ); + $modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) { + return $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documents, + onNext: function (Document $document) use ($plan, &$upserted) { + if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $upserted[] = $document; + } + }, + ); + }); } catch (ConflictException) { throw new Exception($this->getConflictException()); } catch (DuplicateException) { 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 46bf9c3f55..9156599d25 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 @@ -332,7 +332,7 @@ class Create extends Action } }; - $documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions) { + $documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions, $isAPIKey) { $document['$collection'] = $collection->getId(); // Determine the source ID depending on whether it's a bulk operation. @@ -350,6 +350,20 @@ 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) + $createdAt = $document['$createdAt'] ?? null; + $updatedAt = $document['$updatedAt'] ?? null; + if (!$isAPIKey) { + if ($createdAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed'); + } + + if ($updatedAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); + } + } + $document = new Document($document); $setPermissions($document, $permissions); $checkPermissions($collection, $document, Database::PERMISSION_CREATE); @@ -358,9 +372,11 @@ class Create extends Action }, $documents); try { - $dbForProject->createDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documents + $dbForProject->withPreserveDates( + fn () => $dbForProject->createDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documents, + ) ); } catch (DuplicateException) { throw new Exception($this->getDuplicateException()); 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 0be66f0f40..8d0cdc9cec 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,6 +109,18 @@ class Update extends Action throw new Exception($this->getParentNotFoundException()); } + // Allowing to add createdAt and updatedAt timestamps if server side(api key) + $createdAt = $data['$createdAt'] ?? null; + $updatedAt = $data['$updatedAt'] ?? null; + if (!$isAPIKey) { + if ($createdAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed'); + } + + if ($updatedAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); + } + } // Read permission should not be required for update /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); @@ -233,11 +245,11 @@ class Update extends Action try { $document = $dbForProject->withRequestTimestamp( $requestTimestamp, - fn () => $dbForProject->updateDocument( + fn () => $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $document->getId(), $newDocument - ) + )) ); } catch (ConflictException) { throw new Exception($this->getConflictException()); 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 c42b4ee6fd..2864204f93 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 @@ -153,6 +153,18 @@ class Upsert extends Action } } } + // Allowing to add createdAt and updatedAt timestamps if server side(api key) + $createdAt = $data['$createdAt'] ?? null; + $updatedAt = $data['$updatedAt'] ?? null; + if (!$isAPIKey) { + if ($createdAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed'); + } + + if ($updatedAt !== null) { + throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); + } + } $data['$id'] = $documentId; $data['$permissions'] = $permissions ?? []; @@ -236,13 +248,15 @@ class Upsert extends Action $upserted = []; try { - $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - [$newDocument], - onNext: function (Document $document) use (&$upserted) { - $upserted[] = $document; - }, - ); + $dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) { + return $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + [$newDocument], + onNext: function (Document $document) use (&$upserted) { + $upserted[] = $document; + }, + ); + }); } catch (ConflictException) { throw new Exception($this->getConflictException()); } catch (DuplicateException) { diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 6aab125478..f837f05ce1 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -2876,7 +2876,6 @@ trait DatabasesBase 'releaseYear' => 2017, 'birthDay' => '1976-06-12 14:12:55', 'actors' => [], - '$createdAt' => 5 // Should be ignored ], 'permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), @@ -4206,17 +4205,18 @@ trait DatabasesBase $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [ 'data' => [ 'title' => 'Again Updated Date Test', - '$createdAt' => '2022-08-01 13:09:23.040', // $createdAt is not updatable - '$updatedAt' => '2022-08-01 13:09:23.050' // system will update it not api + '$createdAt' => '2022-08-01 13:09:23.040', + '$updatedAt' => '2022-08-01 13:09:23.050' ] ]); + if ($this->getSide() === 'client') { + $this->assertEquals($document['headers']['status-code'], 400); + } else { + $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); + $this->assertEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); + $this->assertEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); - $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); - $this->assertEquals($document['body']['$createdAt'], $createdAt); - $this->assertNotEquals($document['body']['$createdAt'], '2022-08-01 13:09:23.040'); - $this->assertNotEquals($document['body']['$updatedAt'], $updatedAt); - $this->assertNotEquals($document['body']['$updatedAt'], $updatedAtSecond); - $this->assertNotEquals($document['body']['$updatedAt'], '2022-08-01 13:09:23.050'); + } return $data; } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php index 699a2b8f25..23153e8f39 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php @@ -889,4 +889,157 @@ 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/Legacy/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php index 84cd8aeb41..bbede25b59 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php @@ -5243,4 +5243,936 @@ class DatabasesCustomServerTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); } + + public function testDateTimeDocument(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'DateTime Test Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $collection = $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' => 'create_modify_dates', + 'documentSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $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' => 'string', + 'size' => 128, + 'required' => false, + ]); + + // Create datetime attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'datetime', + 'required' => false, + 'format' => 'datetime', + ]); + + sleep(1); + + $date = '2000-01-01T10:00:00.000+00:00'; + + // Test - default behaviour of external datetime attribute not changed + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc1', + 'data' => [ + 'datetime' => '' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + $this->assertNotEmpty($doc['body']['datetime']); + $this->assertNotEmpty($doc['body']['$createdAt']); + $this->assertNotEmpty($doc['body']['$updatedAt']); + + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertNotEmpty($doc['body']['datetime']); + $this->assertNotEmpty($doc['body']['$createdAt']); + $this->assertNotEmpty($doc['body']['$updatedAt']); + + // Test - modifying $createdAt and $updatedAt + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc2', + 'data' => [ + '$createdAt' => $date + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + $this->assertEquals($doc['body']['$createdAt'], $date); + $this->assertNotEmpty($doc['body']['$updatedAt']); + $this->assertNotEquals($doc['body']['$updatedAt'], $date); + + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($doc['body']['$createdAt'], $date); + $this->assertNotEmpty($doc['body']['$updatedAt']); + $this->assertNotEquals($doc['body']['$updatedAt'], $date); + + // 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'] + ])); + } + + public function testSingleDocumentDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Single Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $collection = $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' => 'normal_date_operations', + 'documentSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $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' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + + // Test 1: Create with custom createdAt, then update with custom updatedAt + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc1', + 'data' => [ + 'string' => 'initial', + '$createdAt' => $createDate + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + $this->assertEquals($createDate, $doc['body']['$createdAt']); + $this->assertNotEquals($createDate, $doc['body']['$updatedAt']); + + // Update with custom updatedAt + $updatedDoc = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated', + '$updatedAt' => $updateDate + ] + ]); + + $this->assertEquals(200, $updatedDoc['headers']['status-code']); + $this->assertEquals($createDate, $updatedDoc['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedDoc['body']['$updatedAt']); + + // Test 2: Create with both custom dates + $doc2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc2', + 'data' => [ + 'string' => 'both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc2['headers']['status-code']); + $this->assertEquals($createDate, $doc2['body']['$createdAt']); + $this->assertEquals($updateDate, $doc2['body']['$updatedAt']); + + // Test 3: Create without dates, then update with custom dates + $doc3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc3', + 'data' => [ + 'string' => 'no_dates' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc3['headers']['status-code']); + + $updatedDoc3 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc3', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated_no_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ] + ]); + + $this->assertEquals(200, $updatedDoc3['headers']['status-code']); + $this->assertEquals($createDate, $updatedDoc3['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedDoc3['body']['$updatedAt']); + + // Test 4: Update only createdAt + $doc4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc4', + 'data' => [ + 'string' => 'initial' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc4['headers']['status-code']); + $originalCreatedAt4 = $doc4['body']['$createdAt']; + $originalUpdatedAt4 = $doc4['body']['$updatedAt']; + + $updatedDoc4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc4', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated', + '$updatedAt' => null, + '$createdAt' => null + ], + ]); + + $this->assertEquals(200, $updatedDoc4['headers']['status-code']); + $this->assertEquals($originalCreatedAt4, $updatedDoc4['body']['$createdAt']); + $this->assertNotEquals($originalUpdatedAt4, $updatedDoc4['body']['$updatedAt']); + + // Test 5: Update only updatedAt + $finalDoc4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc4', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'final', + '$updatedAt' => $updateDate, + '$createdAt' => $createDate + ] + ]); + + $this->assertEquals(200, $finalDoc4['headers']['status-code']); + $this->assertEquals($createDate, $finalDoc4['body']['$createdAt']); + $this->assertEquals($updateDate, $finalDoc4['body']['$updatedAt']); + + // Test 6: Create with updatedAt, update with createdAt + $doc5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc5', + 'data' => [ + 'string' => 'doc5', + '$updatedAt' => $date2 + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc5['headers']['status-code']); + $this->assertNotEquals($date2, $doc5['body']['$createdAt']); + $this->assertEquals($date2, $doc5['body']['$updatedAt']); + + $updatedDoc5 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc5', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'doc5_updated', + '$createdAt' => $date1 + ] + ]); + + $this->assertEquals(200, $updatedDoc5['headers']['status-code']); + $this->assertEquals($date1, $updatedDoc5['body']['$createdAt']); + $this->assertNotEquals($date2, $updatedDoc5['body']['$updatedAt']); + + // Test 7: Create with both dates, update with different dates + $doc6 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc6', + 'data' => [ + 'string' => 'doc6', + '$createdAt' => $date1, + '$updatedAt' => $date2 + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $doc6['headers']['status-code']); + $this->assertEquals($date1, $doc6['body']['$createdAt']); + $this->assertEquals($date2, $doc6['body']['$updatedAt']); + + $updatedDoc6 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc6', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'doc6_updated', + '$createdAt' => $date3, + '$updatedAt' => $date3 + ] + ]); + + $this->assertEquals(200, $updatedDoc6['headers']['status-code']); + $this->assertEquals($date3, $updatedDoc6['body']['$createdAt']); + $this->assertEquals($date3, $updatedDoc6['body']['$updatedAt']); + + // 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'] + ])); + } + + public function testBulkDocumentDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Bulk Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $collection = $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' => 'bulk_date_operations', + 'documentSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $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' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + + // Test 1: Bulk create with different date configurations + $documents = [ + [ + '$id' => 'doc1', + 'string' => 'doc1', + '$createdAt' => $createDate, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + [ + '$id' => 'doc2', + 'string' => 'doc2', + '$updatedAt' => $updateDate, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + [ + '$id' => 'doc3', + 'string' => 'doc3', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + [ + '$id' => 'doc4', + 'string' => 'doc4', + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + [ + '$id' => 'doc5', + 'string' => 'doc5', + '$createdAt' => null, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + [ + '$id' => 'doc6', + 'string' => 'doc6', + '$updatedAt' => null, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ] + ]; + + // Create all documents in one bulk operation + $bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => $documents + ]); + + $this->assertEquals(201, $bulkCreateResponse['headers']['status-code']); + $this->assertCount(count($documents), $bulkCreateResponse['body']['documents']); + + // Verify initial state + foreach (['doc1', 'doc3'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id"); + } + + foreach (['doc2', 'doc3'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + } + + foreach (['doc4', 'doc5', 'doc6'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id"); + } + + // Test 2: Bulk update with custom dates + $updateData = [ + 'data' => [ + 'string' => 'updated', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate, + '$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])),] + ], + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateData); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(6, $response['body']['documents']); + + // Verify updated state + foreach (['doc1', 'doc3'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc['body']['string'], "string mismatch for $id"); + } + + foreach (['doc2', 'doc4', 'doc5', 'doc6'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc['body']['string'], "string mismatch for $id"); + } + + $newDate = '2000-03-01T20:45:00.000+00:00'; + $updateDataEnabled = [ + 'data' => [ + 'string' => 'enabled_update', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ], + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateDataEnabled); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(6, $response['body']['documents']); + + // Verify final state + foreach (['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($newDate, $doc['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($newDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('enabled_update', $doc['body']['string'], "string mismatch for $id"); + } + + // 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'] + ])); + } + + public function testUpsertDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Upsert Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $collection = $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' => 'upsert_date_operations', + 'documentSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create string attribute + $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' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + + // Test 1: Upsert new document with custom createdAt + $upsertDoc1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'upsert1', + 'data' => [ + 'string' => 'upsert1_initial', + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + '$createdAt' => $createDate + ], + ]); + + $this->assertEquals(201, $upsertDoc1['headers']['status-code']); + $this->assertEquals($createDate, $upsertDoc1['body']['$createdAt']); + $this->assertNotEquals($createDate, $upsertDoc1['body']['$updatedAt']); + + // Test 2: Upsert existing document with custom updatedAt + $updatedUpsertDoc1 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/upsert1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'upsert1_updated', + '$updatedAt' => $updateDate + ], + ]); + + $this->assertEquals(200, $updatedUpsertDoc1['headers']['status-code']); + $this->assertEquals($createDate, $updatedUpsertDoc1['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedUpsertDoc1['body']['$updatedAt']); + + // Test 3: Upsert new document with both custom dates + $upsertDoc2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'upsert2', + 'data' => [ + 'string' => 'upsert2_both_dates', + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + ]); + + $this->assertEquals(201, $upsertDoc2['headers']['status-code']); + $this->assertEquals($createDate, $upsertDoc2['body']['$createdAt']); + $this->assertEquals($updateDate, $upsertDoc2['body']['$updatedAt']); + + // Test 4: Upsert existing document with different dates + $updatedUpsertDoc2 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/upsert2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'upsert2_updated', + '$createdAt' => $date3, + '$updatedAt' => $date3, + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + ] + ]); + + $this->assertEquals(200, $updatedUpsertDoc2['headers']['status-code']); + $this->assertEquals($date3, $updatedUpsertDoc2['body']['$createdAt']); + $this->assertEquals($date3, $updatedUpsertDoc2['body']['$updatedAt']); + + // Test 5: Bulk upsert operations with custom dates + $upsertDocuments = [ + [ + '$id' => 'bulk_upsert1', + 'string' => 'bulk_upsert1_initial', + '$createdAt' => $createDate + ], + [ + '$id' => 'bulk_upsert2', + 'string' => 'bulk_upsert2_initial', + '$updatedAt' => $updateDate + ], + [ + '$id' => 'bulk_upsert3', + 'string' => 'bulk_upsert3_initial', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + [ + '$id' => 'bulk_upsert4', + 'string' => 'bulk_upsert4_initial' + ] + ]; + + // Create documents using bulk upsert + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => $upsertDocuments + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['documents']); + + // Test 7: Verify initial bulk upsert state + foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id"); + } + + foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + } + + foreach (['bulk_upsert4'] as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id"); + } + + // Test 8: Bulk upsert update with custom dates + $newDate = '2000-04-01T12:00:00.000+00:00'; + $updateUpsertData = [ + 'data' => [ + 'string' => 'bulk_upsert_updated', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ], + 'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()] + ]; + + $upsertIds = ['bulk_upsert1', 'bulk_upsert2', 'bulk_upsert3', 'bulk_upsert4']; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateUpsertData); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['documents']); + + // Verify updated state + foreach ($upsertIds as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertEquals($newDate, $doc['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($newDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('bulk_upsert_updated', $doc['body']['string'], "string mismatch for $id"); + } + + // Test 9: checking by passing null to each + $updateUpsertDataNull = [ + 'data' => [ + 'string' => 'bulk_upsert_null_test', + '$createdAt' => null, + '$updatedAt' => null + ], + 'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()] + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateUpsertDataNull); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['documents']); + + // Verify null handling + foreach ($upsertIds as $id) { + $doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + $this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id"); + } + + + + // 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'] + ])); + } + } From 29973e41822640752be46b78745bba287ec7d2b8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 Aug 2025 03:09:36 +1200 Subject: [PATCH 2/6] Update db + migrations --- composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.lock b/composer.lock index a5b20cd761..5e87399337 100644 --- a/composer.lock +++ b/composer.lock @@ -3497,16 +3497,16 @@ }, { "name": "utopia-php/database", - "version": "0.74.2", + "version": "0.74.3", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "0e9b85b9ce1a2e754c5a644a2b235dc6e23905b3" + "reference": "065b4812799d57fd2c596f88aadd51644247c7e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/0e9b85b9ce1a2e754c5a644a2b235dc6e23905b3", - "reference": "0e9b85b9ce1a2e754c5a644a2b235dc6e23905b3", + "url": "https://api.github.com/repos/utopia-php/database/zipball/065b4812799d57fd2c596f88aadd51644247c7e3", + "reference": "065b4812799d57fd2c596f88aadd51644247c7e3", "shasum": "" }, "require": { @@ -3547,9 +3547,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.74.2" + "source": "https://github.com/utopia-php/database/tree/0.74.3" }, - "time": "2025-07-31T13:40:40+00:00" + "time": "2025-07-31T15:00:30+00:00" }, { "name": "utopia-php/detector", @@ -3997,16 +3997,16 @@ }, { "name": "utopia-php/migration", - "version": "0.13.5", + "version": "0.13.7", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "8142d722d8eeec443e6d5df91b69ecf89b86df8e" + "reference": "fc25d50c3a19e701e905c56a9465143cacb02717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/8142d722d8eeec443e6d5df91b69ecf89b86df8e", - "reference": "8142d722d8eeec443e6d5df91b69ecf89b86df8e", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/fc25d50c3a19e701e905c56a9465143cacb02717", + "reference": "fc25d50c3a19e701e905c56a9465143cacb02717", "shasum": "" }, "require": { @@ -4047,9 +4047,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.13.5" + "source": "https://github.com/utopia-php/migration/tree/0.13.7" }, - "time": "2025-07-29T04:15:45+00:00" + "time": "2025-07-31T15:08:29+00:00" }, { "name": "utopia-php/orchestration", From 983d102ff29ccf4e2f968025f1a6b67292d1a1b2 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 1 Aug 2025 12:31:29 +0530 Subject: [PATCH 3/6] Restrict createdAt and updatedAt attributes for non-API key and non-privileged users in Create, Update, and Upsert actions --- .../Http/Databases/Collections/Documents/Create.php | 12 +++++------- .../Http/Databases/Collections/Documents/Update.php | 8 +++----- .../Http/Databases/Collections/Documents/Upsert.php | 8 +++----- 3 files changed, 11 insertions(+), 17 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 9156599d25..17243caa8f 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 @@ -332,7 +332,7 @@ class Create extends Action } }; - $documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions, $isAPIKey) { + $documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions, $isAPIKey, $isPrivilegedUser) { $document['$collection'] = $collection->getId(); // Determine the source ID depending on whether it's a bulk operation. @@ -351,15 +351,13 @@ 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) - $createdAt = $document['$createdAt'] ?? null; - $updatedAt = $document['$updatedAt'] ?? null; - if (!$isAPIKey) { - if ($createdAt !== null) { + // 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" is not allowed'); } - if ($updatedAt !== null) { + if (isset($document['$updatedAt'])) { throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); } } 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 8d0cdc9cec..17993d47a1 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 @@ -110,14 +110,12 @@ class Update extends Action } // Allowing to add createdAt and updatedAt timestamps if server side(api key) - $createdAt = $data['$createdAt'] ?? null; - $updatedAt = $data['$updatedAt'] ?? null; - if (!$isAPIKey) { - if ($createdAt !== null) { + if (!$isAPIKey && !$isPrivilegedUser) { + if (isset($data['$createdAt'])) { throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed'); } - if ($updatedAt !== null) { + if (isset($data['$updatedAt'])) { throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); } } 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 2864204f93..ecd9ac79df 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 @@ -154,14 +154,12 @@ class Upsert extends Action } } // Allowing to add createdAt and updatedAt timestamps if server side(api key) - $createdAt = $data['$createdAt'] ?? null; - $updatedAt = $data['$updatedAt'] ?? null; - if (!$isAPIKey) { - if ($createdAt !== null) { + if (!$isAPIKey && !$isPrivilegedUser) { + if (isset($data['$createdAt'])) { throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed'); } - if ($updatedAt !== null) { + if (isset($data['$updatedAt'])) { throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed'); } } From 82fbc524f8dfc98ee1a605c9057c45215f39fb2b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 1 Aug 2025 13:53:04 +0530 Subject: [PATCH 4/6] Add comprehensive tests for date and time operations in database rows - Implemented `testDateTimeRow` to validate the behavior of datetime columns, including creation, retrieval, and modification of rows with custom createdAt and updatedAt timestamps. - Added `testSingleRowDateOperations` to cover various scenarios for single row operations with custom date handling. - Created `testBulkRowDateOperations` to test bulk creation and updates of rows with date fields. - Introduced `testUpsertRowDateOperations` to verify upsert functionality with custom date fields. - Cleaned up legacy test file by removing unnecessary blank lines. --- .../Databases/Grids/DatabasesBase.php | 19 +- .../Grids/DatabasesCustomClientTest.php | 154 +++ .../Grids/DatabasesCustomServerTest.php | 943 ++++++++++++++++++ .../Legacy/DatabasesCustomServerTest.php | 3 - 4 files changed, 1107 insertions(+), 12 deletions(-) diff --git a/tests/e2e/Services/Databases/Grids/DatabasesBase.php b/tests/e2e/Services/Databases/Grids/DatabasesBase.php index 882d73f63f..77c5a958ac 100644 --- a/tests/e2e/Services/Databases/Grids/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Grids/DatabasesBase.php @@ -2850,7 +2850,6 @@ trait DatabasesBase 'releaseYear' => 2017, 'birthDay' => '1976-06-12 14:12:55', 'actors' => [], - '$createdAt' => 5 // Should be ignored ], 'permissions' => [ Permission::read(Role::user($this->getUser()['$id'])), @@ -4215,17 +4214,19 @@ trait DatabasesBase $row = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/grids/tables/' . $data['moviesId'] . '/rows/' . $rowId, $headers, [ 'data' => [ 'title' => 'Again Updated Date Test', - '$createdAt' => '2022-08-01 13:09:23.040', // $createdAt is not updatable - '$updatedAt' => '2022-08-01 13:09:23.050' // system will update it not api + '$createdAt' => '2022-08-01 13:09:23.040', + '$updatedAt' => '2022-08-01 13:09:23.050' ] ]); - $this->assertEquals($row['body']['title'], 'Again Updated Date Test'); - $this->assertEquals($row['body']['$createdAt'], $createdAt); - $this->assertNotEquals($row['body']['$createdAt'], '2022-08-01 13:09:23.040'); - $this->assertNotEquals($row['body']['$updatedAt'], $updatedAt); - $this->assertNotEquals($row['body']['$updatedAt'], $updatedAtSecond); - $this->assertNotEquals($row['body']['$updatedAt'], '2022-08-01 13:09:23.050'); + if ($this->getSide() === 'client') { + $this->assertEquals($row['headers']['status-code'], 400); + } else { + $this->assertEquals($row['body']['title'], 'Again Updated Date Test'); + $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')); + + } return $data; } diff --git a/tests/e2e/Services/Databases/Grids/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/Grids/DatabasesCustomClientTest.php index e75039b22e..f07b648890 100644 --- a/tests/e2e/Services/Databases/Grids/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/Grids/DatabasesCustomClientTest.php @@ -890,4 +890,158 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } + + public function testModifyCreatedAtUpdatedAtSingleRow(): 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 . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/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/Grids/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Grids/DatabasesCustomServerTest.php index ba137a0b74..d2c24ca2f1 100644 --- a/tests/e2e/Services/Databases/Grids/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Grids/DatabasesCustomServerTest.php @@ -5189,4 +5189,947 @@ class DatabasesCustomServerTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); } + + public function testDateTimeRow(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'DateTime Test Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'create_modify_dates', + 'rowSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'string', + 'size' => 128, + 'required' => false, + ]); + + // Create datetime column + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/datetime', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'datetime', + 'required' => false, + 'format' => 'datetime', + ]); + + sleep(1); + + $date = '2000-01-01T10:00:00.000+00:00'; + + // Test - default behaviour of external datetime column not changed + $row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row1', + 'data' => [ + 'datetime' => '' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertNotEmpty($row['body']['datetime']); + $this->assertNotEmpty($row['body']['$createdAt']); + $this->assertNotEmpty($row['body']['$updatedAt']); + + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertNotEmpty($row['body']['datetime']); + $this->assertNotEmpty($row['body']['$createdAt']); + $this->assertNotEmpty($row['body']['$updatedAt']); + + // Test - modifying $createdAt and $updatedAt + $row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row2', + 'data' => [ + '$createdAt' => $date + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertEquals($row['body']['$createdAt'], $date); + $this->assertNotEmpty($row['body']['$updatedAt']); + $this->assertNotEquals($row['body']['$updatedAt'], $date); + + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($row['body']['$createdAt'], $date); + $this->assertNotEmpty($row['body']['$updatedAt']); + $this->assertNotEquals($row['body']['$updatedAt'], $date); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testSingleRowDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Single Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'normal_date_operations', + 'rowSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + + // Test 1: Create with custom createdAt, then update with custom updatedAt + $row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row1', + 'data' => [ + 'string' => 'initial', + '$createdAt' => $createDate + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertEquals($createDate, $row['body']['$createdAt']); + $this->assertNotEquals($createDate, $row['body']['$updatedAt']); + + // Update with custom updatedAt + $updatedRow = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated', + '$updatedAt' => $updateDate + ] + ]); + + $this->assertEquals(200, $updatedRow['headers']['status-code']); + $this->assertEquals($createDate, $updatedRow['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedRow['body']['$updatedAt']); + + // Test 2: Create with both custom dates + $row2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row2', + 'data' => [ + 'string' => 'both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row2['headers']['status-code']); + $this->assertEquals($createDate, $row2['body']['$createdAt']); + $this->assertEquals($updateDate, $row2['body']['$updatedAt']); + + // Test 3: Create without dates, then update with custom dates + $row3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row3', + 'data' => [ + 'string' => 'no_dates' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row3['headers']['status-code']); + + $updatedRow3 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row3', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated_no_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ] + ]); + + $this->assertEquals(200, $updatedRow3['headers']['status-code']); + $this->assertEquals($createDate, $updatedRow3['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedRow3['body']['$updatedAt']); + + // Test 4: Update only createdAt + $row4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row4', + 'data' => [ + 'string' => 'initial' + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row4['headers']['status-code']); + $originalCreatedAt4 = $row4['body']['$createdAt']; + $originalUpdatedAt4 = $row4['body']['$updatedAt']; + + $updatedRow4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row4', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'updated', + '$updatedAt' => null, + '$createdAt' => null + ], + ]); + + $this->assertEquals(200, $updatedRow4['headers']['status-code']); + $this->assertEquals($originalCreatedAt4, $updatedRow4['body']['$createdAt']); + $this->assertNotEquals($originalUpdatedAt4, $updatedRow4['body']['$updatedAt']); + + // Test 5: Update only updatedAt + $finalRow4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row4', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'final', + '$updatedAt' => $updateDate, + '$createdAt' => $createDate + ] + ]); + + $this->assertEquals(200, $finalRow4['headers']['status-code']); + $this->assertEquals($createDate, $finalRow4['body']['$createdAt']); + $this->assertEquals($updateDate, $finalRow4['body']['$updatedAt']); + + // Test 6: Create with updatedAt, update with createdAt + $row5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row5', + 'data' => [ + 'string' => 'row5', + '$updatedAt' => $date2 + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row5['headers']['status-code']); + $this->assertNotEquals($date2, $row5['body']['$createdAt']); + $this->assertEquals($date2, $row5['body']['$updatedAt']); + + $updatedRow5 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row5', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'row5_updated', + '$createdAt' => $date1 + ] + ]); + + $this->assertEquals(200, $updatedRow5['headers']['status-code']); + $this->assertEquals($date1, $updatedRow5['body']['$createdAt']); + $this->assertNotEquals($date2, $updatedRow5['body']['$updatedAt']); + + // Test 7: Create with both dates, update with different dates + $row6 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'row6', + 'data' => [ + 'string' => 'row6', + '$createdAt' => $date1, + '$updatedAt' => $date2 + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ] + ]); + + $this->assertEquals(201, $row6['headers']['status-code']); + $this->assertEquals($date1, $row6['body']['$createdAt']); + $this->assertEquals($date2, $row6['body']['$updatedAt']); + + $updatedRow6 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row6', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'row6_updated', + '$createdAt' => $date3, + '$updatedAt' => $date3 + ] + ]); + + $this->assertEquals(200, $updatedRow6['headers']['status-code']); + $this->assertEquals($date3, $updatedRow6['body']['$createdAt']); + $this->assertEquals($date3, $updatedRow6['body']['$updatedAt']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testBulkRowDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Bulk Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'bulk_date_operations', + 'rowSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + + // Test 1: Bulk create with different date configurations + $rows = [ + [ + '$id' => 'row1', + 'string' => 'row1', + '$createdAt' => $createDate, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + [ + '$id' => 'row2', + 'string' => 'row2', + '$updatedAt' => $updateDate, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + [ + '$id' => 'row3', + 'string' => 'row3', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + [ + '$id' => 'row4', + 'string' => 'row4', + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + [ + '$id' => 'row5', + 'string' => 'row5', + '$createdAt' => null, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + [ + '$id' => 'row6', + 'string' => 'row6', + '$updatedAt' => null, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ] + ]; + + // Create all rows in one bulk operation + $bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rows' => $rows + ]); + + $this->assertEquals(201, $bulkCreateResponse['headers']['status-code']); + $this->assertCount(count($rows), $bulkCreateResponse['body']['rows']); + + // Verify initial state + foreach (['row1', 'row3'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id"); + } + + foreach (['row2', 'row3'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + } + + foreach (['row4', 'row5', 'row6'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id"); + } + + // Test 2: Bulk update with custom dates + $updateData = [ + 'data' => [ + 'string' => 'updated', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate, + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateData); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(6, $response['body']['rows']); + + // Verify updated state + foreach (['row1', 'row3'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('updated', $row['body']['string'], "string mismatch for $id"); + } + + foreach (['row2', 'row4', 'row5', 'row6'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('updated', $row['body']['string'], "string mismatch for $id"); + } + + $newDate = '2000-03-01T20:45:00.000+00:00'; + $updateDataEnabled = [ + 'data' => [ + 'string' => 'enabled_update', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ], + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateDataEnabled); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(6, $response['body']['rows']); + + // Verify final state + foreach (['row1', 'row2', 'row3', 'row4', 'row5', 'row6'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($newDate, $row['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($newDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('enabled_update', $row['body']['string'], "string mismatch for $id"); + } + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + + public function testUpsertRowDateOperations(): void + { + $databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Upsert Date Operations Database', + ]); + + $this->assertEquals(201, $databaseId['headers']['status-code']); + $databaseId = $databaseId['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'upsert_date_operations', + 'rowSecurity' => true, + 'permissions' => [], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Create string column + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'string', + 'size' => 128, + 'required' => false, + ]); + + sleep(1); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + + // Test 1: Upsert new row with custom createdAt + $upsertRow1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'upsert1', + 'data' => [ + 'string' => 'upsert1_initial', + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + '$createdAt' => $createDate + ], + ]); + + $this->assertEquals(201, $upsertRow1['headers']['status-code']); + $this->assertEquals($createDate, $upsertRow1['body']['$createdAt']); + $this->assertNotEquals($createDate, $upsertRow1['body']['$updatedAt']); + + // Test 2: Upsert existing row with custom updatedAt + $updatedUpsertRow1 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/upsert1', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'upsert1_updated', + '$updatedAt' => $updateDate + ], + ]); + + $this->assertEquals(200, $updatedUpsertRow1['headers']['status-code']); + $this->assertEquals($createDate, $updatedUpsertRow1['body']['$createdAt']); + $this->assertEquals($updateDate, $updatedUpsertRow1['body']['$updatedAt']); + + // Test 3: Upsert new row with both custom dates + $upsertRow2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'upsert2', + 'data' => [ + 'string' => 'upsert2_both_dates', + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + ]); + + $this->assertEquals(201, $upsertRow2['headers']['status-code']); + $this->assertEquals($createDate, $upsertRow2['body']['$createdAt']); + $this->assertEquals($updateDate, $upsertRow2['body']['$updatedAt']); + + // Test 4: Upsert existing row with different dates + $updatedUpsertRow2 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/upsert2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'string' => 'upsert2_updated', + '$createdAt' => $date3, + '$updatedAt' => $date3, + '$permissions' => [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + ], + ] + ]); + + $this->assertEquals(200, $updatedUpsertRow2['headers']['status-code']); + $this->assertEquals($date3, $updatedUpsertRow2['body']['$createdAt']); + $this->assertEquals($date3, $updatedUpsertRow2['body']['$updatedAt']); + + // Test 5: Bulk upsert operations with custom dates + $upsertRows = [ + [ + '$id' => 'bulk_upsert1', + 'string' => 'bulk_upsert1_initial', + '$createdAt' => $createDate + ], + [ + '$id' => 'bulk_upsert2', + 'string' => 'bulk_upsert2_initial', + '$updatedAt' => $updateDate + ], + [ + '$id' => 'bulk_upsert3', + 'string' => 'bulk_upsert3_initial', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ], + [ + '$id' => 'bulk_upsert4', + 'string' => 'bulk_upsert4_initial' + ] + ]; + + // Create rows using bulk upsert + $response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rows' => $upsertRows + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['rows']); + + // Test 7: Verify initial bulk upsert state + foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id"); + } + + foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + } + + foreach (['bulk_upsert4'] as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id"); + } + + // Test 8: Bulk upsert update with custom dates + $newDate = '2000-04-01T12:00:00.000+00:00'; + $updateUpsertData = [ + 'data' => [ + 'string' => 'bulk_upsert_updated', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ], + 'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()] + ]; + + $upsertIds = ['bulk_upsert1', 'bulk_upsert2', 'bulk_upsert3', 'bulk_upsert4']; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateUpsertData); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['rows']); + + // Verify updated state + foreach ($upsertIds as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals($newDate, $row['body']['$createdAt'], "createdAt mismatch for $id"); + $this->assertEquals($newDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id"); + $this->assertEquals('bulk_upsert_updated', $row['body']['string'], "string mismatch for $id"); + } + + // Test 9: checking by passing null to each + $updateUpsertDataNull = [ + 'data' => [ + 'string' => 'bulk_upsert_null_test', + '$createdAt' => null, + '$updatedAt' => null + ], + 'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()] + ]; + + // Use bulk update instead of individual updates + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $updateUpsertDataNull); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(4, $response['body']['rows']); + + // Verify null handling + foreach ($upsertIds as $id) { + $row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id"); + $this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id"); + } + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/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/Legacy/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php index bbede25b59..3d61b529fb 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php @@ -6159,8 +6159,6 @@ class DatabasesCustomServerTest extends Scope $this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id"); } - - // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ 'content-type' => 'application/json', @@ -6174,5 +6172,4 @@ class DatabasesCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } - } From 67582b103ce92b3c5f439c87c2020fde2cf3599c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 1 Aug 2025 13:57:38 +0530 Subject: [PATCH 5/6] updated composer --- composer.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index 84de95fd24..72200f7531 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "43a92af943693f80341a8582cf582986", + "content-hash": "c4ae112243a897e16552c507c5e94097", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "15.0.0", + "version": "15.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf" + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/deb97b62e0abed8a4fd5c5d48e77365cf89867cf", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b", + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b", "shasum": "" }, "require": { @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/15.0.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-05-18T09:47:10+00:00" + "time": "2025-08-01T04:50:51+00:00" }, { "name": "appwrite/php-clamav", @@ -4814,16 +4814,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.41.26", + "version": "0.41.27", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b" + "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b", - "reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/083fd2e8163d6a4e59ee971ac6cb97277d831dd5", + "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5", "shasum": "" }, "require": { @@ -4859,9 +4859,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.41.26" + "source": "https://github.com/appwrite/sdk-generator/tree/0.41.27" }, - "time": "2025-07-30T06:53:12+00:00" + "time": "2025-07-31T10:20:46+00:00" }, { "name": "doctrine/annotations", From f6c4457398ef0cbcbf5e10e1df2c7f7e4649920a Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 1 Aug 2025 14:54:15 +0530 Subject: [PATCH 6/6] changed the error message for client side --- .../Databases/Http/Databases/Collections/Documents/Create.php | 4 ++-- .../Databases/Http/Databases/Collections/Documents/Update.php | 4 ++-- .../Databases/Http/Databases/Collections/Documents/Upsert.php | 4 ++-- 3 files changed, 6 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 17243caa8f..9e7a31c4be 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 @@ -354,11 +354,11 @@ class Create extends Action // 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" is not allowed'); + 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" is not allowed'); + throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); } } 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 17993d47a1..1d06e6d0da 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 @@ -112,11 +112,11 @@ class Update 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" is not allowed'); + 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" is not allowed'); + 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 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 ecd9ac79df..e862896a40 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 @@ -156,11 +156,11 @@ 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" is not allowed'); + 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" is not allowed'); + throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); } }