From b452bd0d44df1a34cebf69902df9c7f9910dfd17 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 17 Sep 2025 13:54:27 +0530 Subject: [PATCH] updated endpoints --- composer.json | 2 +- composer.lock | 54 ++-- .../Collections/Documents/Action.php | 2 + .../Documents/Attribute/Decrement.php | 10 +- .../Documents/Attribute/Increment.php | 10 +- .../Collections/Documents/Bulk/Upsert.php | 2 +- .../Collections/Documents/Upsert.php | 2 +- .../Platform/Workers/StatsResources.php | 2 +- .../Utopia/Response/Model/Document.php | 12 + tests/e2e/Scopes/Scope.php | 6 + .../Databases/Legacy/DatabasesBase.php | 183 +++++++++++++ .../Legacy/DatabasesCustomServerTest.php | 242 ++++++++++++++++++ .../Databases/TablesDB/DatabasesBase.php | 182 +++++++++++++ .../TablesDB/DatabasesCustomServerTest.php | 242 ++++++++++++++++++ 14 files changed, 921 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index 1dc7288441..4f0dbed48d 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": "1.*", + "utopia-php/database": "dev-dat-677 as 1.4.9", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dns": "0.3.*", diff --git a/composer.lock b/composer.lock index e454de0313..dcd888a147 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": "7553e976312b0423cc31544abb91caec", + "content-hash": "bf86eeb3175aac99688369676b4d3f84", "packages": [ { "name": "adhocore/jwt", @@ -756,24 +756,21 @@ }, { "name": "google/protobuf", - "version": "v4.32.0", + "version": "v4.32.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646" + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", "shasum": "" }, "require": { "php": ">=8.1.0" }, - "provide": { - "ext-protobuf": "*" - }, "require-dev": { "phpunit/phpunit": ">=5.0.0 <8.5.27" }, @@ -797,9 +794,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.0" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1" }, - "time": "2025-08-14T20:00:33+00:00" + "time": "2025-09-14T05:14:52+00:00" }, { "name": "league/csv", @@ -3638,16 +3635,16 @@ }, { "name": "utopia-php/database", - "version": "1.4.8", + "version": "dev-dat-677", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a" + "reference": "e95802fb07225a1616e5a242c379e8910e2f724a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a", - "reference": "dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e95802fb07225a1616e5a242c379e8910e2f724a", + "reference": "e95802fb07225a1616e5a242c379e8910e2f724a", "shasum": "" }, "require": { @@ -3688,9 +3685,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.4.8" + "source": "https://github.com/utopia-php/database/tree/dat-677" }, - "time": "2025-09-12T03:35:59+00:00" + "time": "2025-09-16T18:08:23+00:00" }, { "name": "utopia-php/detector", @@ -6236,16 +6233,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.26", + "version": "9.6.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a0139ea157533454f611038326f3020b3051f129" + "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a0139ea157533454f611038326f3020b3051f129", - "reference": "a0139ea157533454f611038326f3020b3051f129", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0a9aa4440b6a9528cf360071502628d717af3e0a", + "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a", "shasum": "" }, "require": { @@ -6319,7 +6316,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.26" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.27" }, "funding": [ { @@ -6343,7 +6340,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T06:17:45+00:00" + "time": "2025-09-14T06:18:03+00:00" }, { "name": "psr/cache", @@ -8510,9 +8507,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-dat-677", + "alias": "1.4.9", + "alias_normalized": "1.4.9.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/database": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index d1d0738990..be7d3500ed 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -37,6 +37,8 @@ abstract class Action extends AppwriteAction 'privileged' => [ '$createdAt', '$updatedAt', + '$createdBy', + '$updatedBy' ], ]; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index 289dee5a8d..677adad2cc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -23,6 +23,7 @@ use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\Numeric; +use Utopia\Database\Document; class Decrement extends Action { @@ -77,12 +78,13 @@ class Decrement extends Action ->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true) ->inject('response') ->inject('dbForProject') + ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { @@ -114,6 +116,12 @@ class Decrement extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); } + // Set $updatedBy field to current user ID if available + $userId = $user->getId(); + if (!empty($userId)) { + $document->setAttribute('$updatedBy', $userId); + } + $relationships = \array_map( fn ($document) => $document->getAttribute('key'), \array_filter( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index fe8bd2d225..ab9ffeea3b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -23,6 +23,7 @@ use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\Numeric; +use Utopia\Database\Document; class Increment extends Action { @@ -77,12 +78,13 @@ class Increment extends Action ->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true) ->inject('response') ->inject('dbForProject') + ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { @@ -114,6 +116,12 @@ class Increment extends Action throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); } + // Set $updatedBy field to current user ID if available + $userId = $user->getId(); + if (!empty($userId)) { + $document->setAttribute('$updatedBy', $userId); + } + $relationships = \array_map( fn ($document) => $document->getAttribute('key'), \array_filter( 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 395e3d757b..253d7dab3b 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 @@ -113,7 +113,7 @@ class Upsert extends Action try { $modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) { - return $dbForProject->createOrUpdateDocuments( + return $dbForProject->upsertDocuments( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documents, onNext: function (Document $document) use ($plan, &$upserted) { 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 54b1cad950..9aa138b31d 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 @@ -243,7 +243,7 @@ class Upsert extends Action $upserted = []; try { $dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) { - return $dbForProject->createOrUpdateDocuments( + return $dbForProject->upsertDocuments( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), [$newDocument], onNext: function (Document $document) use (&$upserted) { diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index da8c086bf4..1cd866e9a9 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -435,7 +435,7 @@ class StatsResources extends Action $message = 'Stats writeDocuments project: ' . $project->getId() . '(' . $project->getSequence() . ')'; try { - $dbForLogs->createOrUpdateDocuments( + $dbForLogs->upsertDocuments( 'stats', $this->documents ); diff --git a/src/Appwrite/Utopia/Response/Model/Document.php b/src/Appwrite/Utopia/Response/Model/Document.php index 5bad504a63..01d8e0cf94 100644 --- a/src/Appwrite/Utopia/Response/Model/Document.php +++ b/src/Appwrite/Utopia/Response/Model/Document.php @@ -69,6 +69,18 @@ class Document extends Any 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('$createdBy', [ + 'type' => self::TYPE_STRING, + 'description' => 'User ID of the user who created the document.', + 'default' => null, + 'example' => '5e5ea5c16897e', + ]) + ->addRule('$updatedBy', [ + 'type' => self::TYPE_STRING, + 'description' => 'User ID of the user who updated the document.', + 'default' => null, + 'example' => '5e5ea5c16897e', + ]) ->addRule('$permissions', [ 'type' => self::TYPE_STRING, 'description' => 'Document permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 5b7f1a8771..fccbecfcd5 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -7,6 +7,8 @@ use Appwrite\Tests\Retryable; use PHPUnit\Framework\TestCase; use Tests\E2E\Client; use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization; use Utopia\System\System; abstract class Scope extends TestCase @@ -40,6 +42,10 @@ abstract class Scope extends TestCase protected function tearDown(): void { $this->client = null; + + // Clean up Authorization context to prevent state contamination between tests + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); } protected function getLastEmail(int $limit = 1): array diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 8d27aa7230..b09d1f600d 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -4354,6 +4354,189 @@ trait DatabasesBase return $data; } + public function testCreatedByUpdatedBy(): void + { + // Setup: Create database for createdBy/updatedBy tests + $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' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Setup: Create collection for createdBy/updatedBy tests + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'CreatedBy UpdatedBy Collection', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Setup: Add attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'releaseYear', + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'count', + 'required' => false, + ]); + + // Wait for attributes to be created + sleep(2); + + $headers = $this->getSide() === 'client' ? array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()): ['x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey']]; + + // Test 1: Create document - verify $createdBy and $updatedBy behavior + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [ + 'documentId' => ID::unique(), + 'data' => [ + 'title' => 'CreatedBy UpdatedBy Test', + 'releaseYear' => 2024 + ] + ]); + + $this->assertEquals(201, $document['headers']['status-code']); + $this->assertEquals('CreatedBy UpdatedBy Test', $document['body']['title']); + + $documentId = $document['body']['$id']; + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $document['body']['$createdBy']); + $this->assertEquals($user['$id'], $document['body']['$updatedBy']); + + \sleep(1); + + // Test 2: Update document - verify $updatedBy behavior + $updatedDocument = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers, [ + 'data' => [ + 'title' => 'Updated CreatedBy UpdatedBy Test', + ] + ]); + + $this->assertEquals(200, $updatedDocument['headers']['status-code']); + $this->assertEquals('Updated CreatedBy UpdatedBy Test', $updatedDocument['body']['title']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $updatedDocument['body']['$createdBy']); + $this->assertEquals($user['$id'], $updatedDocument['body']['$updatedBy']); + + // Test 4: Upsert operation + $upsertDocument = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers, [ + 'data' => [ + 'title' => 'Upserted Document', + 'releaseYear' => 2025 + ] + ]); + + $this->assertEquals(200, $upsertDocument['headers']['status-code']); + $this->assertEquals('Upserted Document', $upsertDocument['body']['title']); + + // Client side: $createdBy should remain original user, $updatedBy should be current user + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $upsertDocument['body']['$createdBy']); + $this->assertEquals($user['$id'], $upsertDocument['body']['$updatedBy']); + + // Test 5: Create new document with upsert + $newUpsertId = ID::unique(); + $newUpsertDocument = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $newUpsertId, $headers, [ + 'data' => [ + 'title' => 'New Upserted Document', + 'releaseYear' => 2026 + ] + ]); + + $this->assertEquals(200, $newUpsertDocument['headers']['status-code']); + $this->assertEquals('New Upserted Document', $newUpsertDocument['body']['title']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $newUpsertDocument['body']['$createdBy']); + $this->assertEquals($user['$id'], $newUpsertDocument['body']['$updatedBy']); + + // Test 6: Increment/Decrement operations + $countDocument = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [ + 'documentId' => ID::unique(), + 'data' => [ + 'title' => 'Count Test Document', + 'count' => 10 + ] + ]); + + $this->assertEquals(201, $countDocument['headers']['status-code']); + $countDocumentId = $countDocument['body']['$id']; + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $countDocument['body']['$createdBy']); + $this->assertEquals($user['$id'], $countDocument['body']['$updatedBy']); + + // Test increment operation + $incrementResponse = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $countDocumentId . '/count/increment', $headers, [ + 'value' => 5 + ]); + + $this->assertEquals(200, $incrementResponse['headers']['status-code']); + $this->assertEquals(15, $incrementResponse['body']['count']); + + // Client side: $createdBy should remain unchanged, $updatedBy should be current user + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $incrementResponse['body']['$createdBy']); + $this->assertEquals($user['$id'], $incrementResponse['body']['$updatedBy']); + + // Test decrement operation + $decrementResponse = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $countDocumentId . '/count/decrement', $headers, [ + 'value' => 3 + ]); + + $this->assertEquals(200, $decrementResponse['headers']['status-code']); + $this->assertEquals(12, $decrementResponse['body']['count']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $decrementResponse['body']['$createdBy']); + $this->assertEquals($user['$id'], $decrementResponse['body']['$updatedBy']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $headers); + } + public function testUpdatePermissionsWithEmptyPayload(): array { // Create Database diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php index c1ce75c38d..6c11e752ca 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php @@ -6729,4 +6729,246 @@ class DatabasesCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } + + public function testCreatedByUpdatedByModify(): void + { + // Create database + $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' => 'CreatedBy UpdatedBy Test DB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'CreatedBy UpdatedBy Collection', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add attributes + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'count', + 'required' => false, + ]); + + sleep(2); + + // Test 1: Create document with manual $createdBy and $updatedBy + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'title' => 'Server Side Test', + 'count' => 10, + '$createdBy' => 'user-123', + '$updatedBy' => 'user-456' + ] + ]); + + $this->assertEquals(201, $document['headers']['status-code']); + $this->assertEquals('user-123', $document['body']['$createdBy']); + $this->assertEquals('user-456', $document['body']['$updatedBy']); + $documentId = $document['body']['$id']; + + // Test 2: Update document with different $updatedBy + $updatedDocument = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'data' => [ + 'title' => 'Updated Server Side Test', + '$updatedBy' => 'user-789' + ] + ]); + + $this->assertEquals(200, $updatedDocument['headers']['status-code']); + $this->assertEquals('user-123', $updatedDocument['body']['$createdBy']); // Should remain unchanged + $this->assertEquals('user-789', $updatedDocument['body']['$updatedBy']); // Should be updated + + // Test 3: Bulk create with mixed $createdBy and $updatedBy values + // bulk1: both createdBy and updatedBy set + // bulk2: only createdBy set, updatedBy should be null + // bulk3: neither set, both should be null + $bulkDocuments = [ + [ + '$id' => 'bulk1', + 'title' => 'Bulk Document 1', + 'count' => 1, + '$createdBy' => 'bulk-user-1', + '$updatedBy' => 'bulk-user-1' + ], + [ + '$id' => 'bulk2', + 'title' => 'Bulk Document 2', + 'count' => 2, + '$createdBy' => 'bulk-user-2' + ], + [ + '$id' => 'bulk3', + 'title' => 'Bulk Document 3', + 'count' => 3 + ] + ]; + + $bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documents' => $bulkDocuments + ]); + + $this->assertEquals(201, $bulkCreateResponse['headers']['status-code']); + $this->assertCount(3, $bulkCreateResponse['body']['documents']); + + // Verify bulk create results + $this->assertEquals('bulk-user-1', $bulkCreateResponse['body']['documents'][0]['$createdBy']); + $this->assertEquals('bulk-user-1', $bulkCreateResponse['body']['documents'][0]['$updatedBy']); + $this->assertEquals('bulk-user-2', $bulkCreateResponse['body']['documents'][1]['$createdBy']); + + // Test 4: Bulk update with $updatedBy + $bulkUpdateResponse = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'data' => [ + 'title' => 'Bulk Updated', + '$updatedBy' => 'bulk-updater' + ], + 'queries' => [Query::startsWith('$id', 'bulk')->toString()] + ]); + + $this->assertEquals(200, $bulkUpdateResponse['headers']['status-code']); + $this->assertCount(3, $bulkUpdateResponse['body']['documents']); + + // Verify bulk update results - $createdBy should remain unchanged, $updatedBy should be updated + foreach ($bulkUpdateResponse['body']['documents'] as $doc) { + $this->assertEquals('bulk-updater', $doc['$updatedBy']); + $this->assertEquals('Bulk Updated', $doc['title']); + + // $createdBy should remain as originally set + if ($doc['$id'] === 'bulk1') { + $this->assertEquals('bulk-user-1', $doc['$createdBy']); + } elseif ($doc['$id'] === 'bulk2') { + $this->assertEquals('bulk-user-2', $doc['$createdBy']); + } + } + + // Test 5: Bulk upsert with $createdBy and $updatedBy + // bulk1: existing document - createdBy should be ignored, updatedBy should be updated + // new-upsert: new document - both should be set as specified + $upsertDocuments = [ + [ + '$id' => 'bulk1', // Existing document + 'title' => 'Upserted Document 1', + 'count' => 100, + '$createdBy' => 'should-not-change', + '$updatedBy' => 'upsert-updater-1' + ], + [ + '$id' => 'new-upsert', + 'title' => 'New Upserted Document', + 'count' => 200, + '$createdBy' => 'new-creator', + '$updatedBy' => 'new-updater' + ] + ]; + + $bulkUpsertResponse = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documents' => $upsertDocuments + ]); + + $this->assertEquals(200, $bulkUpsertResponse['headers']['status-code']); + $this->assertCount(2, $bulkUpsertResponse['body']['documents']); + + // Verify upsert results + foreach ($bulkUpsertResponse['body']['documents'] as $doc) { + if ($doc['$id'] === 'bulk1') { + $this->assertEquals('should-not-change', $doc['$createdBy']); + $this->assertEquals('upsert-updater-1', $doc['$updatedBy']); + } elseif ($doc['$id'] === 'new-upsert') { + $this->assertEquals('new-creator', $doc['$createdBy']); + $this->assertEquals('new-updater', $doc['$updatedBy']); + } + } + + // Test 6: Increment operation + // making requests via root + $incrementResponse = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/increment', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'value' => 5 + ]); + + $this->assertEquals(200, $incrementResponse['headers']['status-code']); + $this->assertEquals(15, $incrementResponse['body']['count']); + $this->assertEquals('user-123', $incrementResponse['body']['$createdBy']); + $this->assertEquals($this->getRoot()['$id'], $incrementResponse['body']['$updatedBy']); + + // Test 7: Decrement operation + $decrementResponse = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/decrement', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'value' => 3 + ]); + + $this->assertEquals(200, $decrementResponse['headers']['status-code']); + $this->assertEquals(12, $decrementResponse['body']['count']); + $this->assertEquals('user-123', $decrementResponse['body']['$createdBy']); + $this->assertEquals($this->getRoot()['$id'], $decrementResponse['body']['$updatedBy']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + } } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index eba7ec96a7..7d3754a9e9 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -8828,4 +8828,186 @@ trait DatabasesBase $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}", $headers); } + public function testCreatedByUpdatedBy(): void + { + // Setup: Create database for createdBy/updatedBy tests + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Database' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Setup: Create table for createdBy/updatedBy tests + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'tableId' => ID::unique(), + 'name' => 'CreatedBy UpdatedBy Table', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Setup: Add columns + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'releaseYear', + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'count', + 'required' => false, + ]); + + // Wait for columns to be created + sleep(2); + + $headers = array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + // Test 1: Create row - verify $createdBy and $updatedBy behavior + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $headers, [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'CreatedBy UpdatedBy Test', + 'releaseYear' => 2024 + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertEquals('CreatedBy UpdatedBy Test', $row['body']['title']); + + $rowId = $row['body']['$id']; + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $row['body']['$createdBy']); + $this->assertEquals($user['$id'], $row['body']['$updatedBy']); + + \sleep(1); + + // Test 2: Update row - verify $updatedBy behavior + $updatedRow = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $headers, [ + 'data' => [ + 'title' => 'Updated CreatedBy UpdatedBy Test', + ] + ]); + + $this->assertEquals(200, $updatedRow['headers']['status-code']); + $this->assertEquals('Updated CreatedBy UpdatedBy Test', $updatedRow['body']['title']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $updatedRow['body']['$createdBy']); + $this->assertEquals($user['$id'], $updatedRow['body']['$updatedBy']); + + // Test 4: Upsert operation + $upsertRow = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $headers, [ + 'data' => [ + 'title' => 'Upserted Row', + 'releaseYear' => 2025 + ] + ]); + + $this->assertEquals(200, $upsertRow['headers']['status-code']); + $this->assertEquals('Upserted Row', $upsertRow['body']['title']); + + // Client side: $createdBy should remain original user, $updatedBy should be current user + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $upsertRow['body']['$createdBy']); + $this->assertEquals($user['$id'], $upsertRow['body']['$updatedBy']); + + // Test 5: Create new row with upsert + $newUpsertId = ID::unique(); + $newUpsertRow = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $newUpsertId, $headers, [ + 'data' => [ + 'title' => 'New Upserted Row', + 'releaseYear' => 2026 + ] + ]); + + $this->assertEquals(200, $newUpsertRow['headers']['status-code']); + $this->assertEquals('New Upserted Row', $newUpsertRow['body']['title']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $newUpsertRow['body']['$createdBy']); + $this->assertEquals($user['$id'], $newUpsertRow['body']['$updatedBy']); + + // Test 6: Increment/Decrement operations + $countRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $headers, [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'Count Test Row', + 'count' => 10 + ] + ]); + + $this->assertEquals(201, $countRow['headers']['status-code']); + $countRowId = $countRow['body']['$id']; + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $countRow['body']['$createdBy']); + $this->assertEquals($user['$id'], $countRow['body']['$updatedBy']); + + // Test increment operation + $incrementResponse = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $countRowId . '/count/increment', $headers, [ + 'value' => 5 + ]); + + $this->assertEquals(200, $incrementResponse['headers']['status-code']); + $this->assertEquals(15, $incrementResponse['body']['count']); + + // Client side: $createdBy should remain unchanged, $updatedBy should be current user + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $incrementResponse['body']['$createdBy']); + $this->assertEquals($user['$id'], $incrementResponse['body']['$updatedBy']); + + // Test decrement operation + $decrementResponse = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $countRowId . '/count/decrement', $headers, [ + 'value' => 3 + ]); + + $this->assertEquals(200, $decrementResponse['headers']['status-code']); + $this->assertEquals(12, $decrementResponse['body']['count']); + + $user = $this->getSide() === 'client' ?$this->getUser() : $this->getRoot(); + $this->assertEquals($user['$id'], $decrementResponse['body']['$createdBy']); + $this->assertEquals($user['$id'], $decrementResponse['body']['$updatedBy']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, $headers); + } + } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php index 5e35fa065d..f48bf3c183 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php @@ -6697,4 +6697,246 @@ class DatabasesCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } + + public function testCreatedByUpdatedByModify(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'CreatedBy UpdatedBy Test DB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create table + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'tableId' => ID::unique(), + 'name' => 'CreatedBy UpdatedBy Table', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Add columns + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'count', + 'required' => false, + ]); + + sleep(2); + + // Test 1: Create row with manual $createdBy and $updatedBy + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'rowId' => ID::unique(), + 'data' => [ + 'title' => 'Server Side Test', + 'count' => 10, + '$createdBy' => 'user-123', + '$updatedBy' => 'user-456' + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + $this->assertEquals('user-123', $row['body']['$createdBy']); + $this->assertEquals('user-456', $row['body']['$updatedBy']); + $rowId = $row['body']['$id']; + + // Test 2: Update row with different $updatedBy + $updatedRow = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'data' => [ + 'title' => 'Updated Server Side Test', + '$updatedBy' => 'user-789' + ] + ]); + + $this->assertEquals(200, $updatedRow['headers']['status-code']); + $this->assertEquals('user-123', $updatedRow['body']['$createdBy']); // Should remain unchanged + $this->assertEquals('user-789', $updatedRow['body']['$updatedBy']); // Should be updated + + // Test 3: Bulk create with mixed $createdBy and $updatedBy values + // bulk1: both createdBy and updatedBy set + // bulk2: only createdBy set, updatedBy should be null + // bulk3: neither set, both should be null + $bulkRows = [ + [ + '$id' => 'bulk1', + 'title' => 'Bulk Row 1', + 'count' => 1, + '$createdBy' => 'bulk-user-1', + '$updatedBy' => 'bulk-user-1' + ], + [ + '$id' => 'bulk2', + 'title' => 'Bulk Row 2', + 'count' => 2, + '$createdBy' => 'bulk-user-2' + ], + [ + '$id' => 'bulk3', + 'title' => 'Bulk Row 3', + 'count' => 3 + ] + ]; + + $bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'rows' => $bulkRows + ]); + + $this->assertEquals(201, $bulkCreateResponse['headers']['status-code']); + $this->assertCount(3, $bulkCreateResponse['body']['rows']); + + // Verify bulk create results + $this->assertEquals('bulk-user-1', $bulkCreateResponse['body']['rows'][0]['$createdBy']); + $this->assertEquals('bulk-user-1', $bulkCreateResponse['body']['rows'][0]['$updatedBy']); + $this->assertEquals('bulk-user-2', $bulkCreateResponse['body']['rows'][1]['$createdBy']); + + // Test 4: Bulk update with $updatedBy + $bulkUpdateResponse = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'data' => [ + 'title' => 'Bulk Updated', + '$updatedBy' => 'bulk-updater' + ], + 'queries' => [Query::startsWith('$id', 'bulk')->toString()] + ]); + + $this->assertEquals(200, $bulkUpdateResponse['headers']['status-code']); + $this->assertCount(3, $bulkUpdateResponse['body']['rows']); + + // Verify bulk update results - $createdBy should remain unchanged, $updatedBy should be updated + foreach ($bulkUpdateResponse['body']['rows'] as $doc) { + $this->assertEquals('bulk-updater', $doc['$updatedBy']); + $this->assertEquals('Bulk Updated', $doc['title']); + + // $createdBy should remain as originally set + if ($doc['$id'] === 'bulk1') { + $this->assertEquals('bulk-user-1', $doc['$createdBy']); + } elseif ($doc['$id'] === 'bulk2') { + $this->assertEquals('bulk-user-2', $doc['$createdBy']); + } + } + + // Test 5: Bulk upsert with $createdBy and $updatedBy + // bulk1: existing row - createdBy should be ignored, updatedBy should be updated + // new-upsert: new row - both should be set as specified + $upsertRows = [ + [ + '$id' => 'bulk1', // Existing row + 'title' => 'Upserted Row 1', + 'count' => 100, + '$createdBy' => 'should-not-change', + '$updatedBy' => 'upsert-updater-1' + ], + [ + '$id' => 'new-upsert', + 'title' => 'New Upserted Row', + 'count' => 200, + '$createdBy' => 'new-creator', + '$updatedBy' => 'new-updater' + ] + ]; + + $bulkUpsertResponse = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'rows' => $upsertRows + ]); + + $this->assertEquals(200, $bulkUpsertResponse['headers']['status-code']); + $this->assertCount(2, $bulkUpsertResponse['body']['rows']); + + // Verify upsert results + foreach ($bulkUpsertResponse['body']['rows'] as $doc) { + if ($doc['$id'] === 'bulk1') { + $this->assertEquals('should-not-change', $doc['$createdBy']); + $this->assertEquals('upsert-updater-1', $doc['$updatedBy']); + } elseif ($doc['$id'] === 'new-upsert') { + $this->assertEquals('new-creator', $doc['$createdBy']); + $this->assertEquals('new-updater', $doc['$updatedBy']); + } + } + + // Test 6: Increment operation + // making requests via root + $incrementResponse = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId . '/count/increment', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'value' => 5 + ]); + + $this->assertEquals(200, $incrementResponse['headers']['status-code']); + $this->assertEquals(15, $incrementResponse['body']['count']); + $this->assertEquals('user-123', $incrementResponse['body']['$createdBy']); + $this->assertEquals($this->getRoot()['$id'], $incrementResponse['body']['$updatedBy']); + + // Test 7: Decrement operation + $decrementResponse = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId . '/count/decrement', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'value' => 3 + ]); + + $this->assertEquals(200, $decrementResponse['headers']['status-code']); + $this->assertEquals(12, $decrementResponse['body']['count']); + $this->assertEquals('user-123', $decrementResponse['body']['$createdBy']); + $this->assertEquals($this->getRoot()['$id'], $decrementResponse['body']['$updatedBy']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + } }