From a2e9ee73dc6b52629ebaeb6f63d642d505cdd92c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 3 Sep 2025 19:36:24 +0530 Subject: [PATCH 1/4] bulk upsert support --- .../Collections/Documents/Bulk/Upsert.php | 18 ++++++- .../Http/TablesDB/Tables/Rows/Bulk/Upsert.php | 4 ++ .../Realtime/RealtimeCustomClientTest.php | 50 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) 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 3c6e5ddc57..bc26d4b4ad 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 @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk; +use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action; @@ -74,11 +75,15 @@ class Upsert extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -141,5 +146,16 @@ class Upsert extends Action 'total' => $modified, $this->getSdkGroup() => $upserted ]), $this->getResponseModel()); + + $this->triggerBulk( + 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert', + $database, + $collection, + $documents, + $queueForEvents, + $queueForRealtime, + $queueForFunctions, + $queueForWebhooks + ); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php index c4a7c6e677..26c5c8030c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php @@ -61,6 +61,10 @@ class Upsert extends DocumentsUpsert ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 60f58bd8ee..3121f9b45f 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1188,6 +1188,56 @@ class RealtimeCustomClientTest extends Scope $this->assertArrayHasKey('$permissions', $response['data']['payload']); $this->assertIsArray($response['data']['payload']['$permissions']); + // bulk upsert + $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Robert Downey Jr.', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ] + ], + ]); + + $response = json_decode($client->receive(), true); + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + $client->close(); } From b7a0f5399470a121dd533390fcae5b33b8f067a5 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 3 Sep 2025 19:43:33 +0530 Subject: [PATCH 2/4] updated tests --- tests/e2e/Services/Realtime/RealtimeCustomClientTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 3121f9b45f..e83680db1e 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1207,7 +1207,6 @@ class RealtimeCustomClientTest extends Scope ], ]); - $response = json_decode($client->receive(), true); $response = json_decode($client->receive(), true); $this->assertArrayHasKey('type', $response); $this->assertArrayHasKey('data', $response); From fdeb8c48c5c350d86cd09e04ea663df52bd4ff27 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 3 Sep 2025 19:53:20 +0530 Subject: [PATCH 3/4] added permission upsert test --- .../Realtime/RealtimeCustomClientTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index e83680db1e..3e57c5e9bc 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1693,6 +1693,107 @@ class RealtimeCustomClientTest extends Scope $this->assertIsArray($response2['data']['payload']['$permissions']); } + // bulk upsert + $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => ID::unique(), + 'name' => 'Robert Downey Jr.', + '$permissions' => [ + Permission::read(Role::user($user1Id)), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'Thor', + '$permissions' => [ + Permission::read(Role::user($user2Id)), + ], + ] + ], + ]); + + $response = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + + // client1 shouldnot receive more than 1 event + try { + json_decode(json_decode($client1->receive(), true)); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + $response = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertCount(6, $response['data']['channels']); + + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.*.collections.*", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']); + $this->assertContains("databases.*", $response['data']['events']); + + $this->assertNotEmpty($response['data']['payload']); + $this->assertIsArray($response['data']['payload']); + $this->assertArrayHasKey('$id', $response['data']['payload']); + $this->assertArrayHasKey('name', $response['data']['payload']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + $this->assertIsArray($response['data']['payload']['$permissions']); + + // client2 shouldnot receive more than 1 event + try { + json_decode(json_decode($client2->receive(), true)); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + $client1->close(); $client2->close(); } From 52f8cb75cac4133d019bfd4573cfe9c27ffa6c2c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k <83803257+ArnabChatterjee20k@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:27:23 +0530 Subject: [PATCH 4/4] Update Upsert.php --- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bc26d4b4ad..395e3d757b 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 @@ -151,7 +151,7 @@ class Upsert extends Action 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert', $database, $collection, - $documents, + $upserted, $queueForEvents, $queueForRealtime, $queueForFunctions,