diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 2c38811022..2557bf3f26 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -383,6 +383,8 @@ class Event { $this->params = []; $this->sensitive = []; + $this->event = ''; + $this->payload = []; return $this; } 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 9a1c5f3dad..7e9aafad75 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 @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; +use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Utopia\Database\Database; use Utopia\Database\Document; @@ -279,4 +280,59 @@ abstract class Action extends UtopiaAction return true; } + + /** + * For triggering different queues for each document for a bulk documents + * @param string $event + * @param Document $database + * @param Document $collection + * @param Document[] $documents + * @param Event $queueForEvents + * @param Event $queueForRealtime + * @param Event $queueForFunctions + * @param Event $queueForWebhooks + * @return void + */ + protected function triggerBulk( + string $event, + Document $database, + Document $collection, + array $documents, + Event $queueForEvents, + Event $queueForRealtime, + Event $queueForFunctions, + Event $queueForWebhooks + ): void { + $queueForEvents + ->setEvent($event) + ->setParam('databaseId', $database->getId()) + ->setContext('database', $database) + ->setParam('collectionId', $collection->getId()) + ->setParam('tableId', $collection->getId()) + ->setContext($this->getCollectionsEventsContext(), $collection); + + foreach ($documents as $document) { + $queueForEvents + ->setParam('documentId', $document->getId()) + ->setParam('rowId', $document->getId()) + ->setPayload($document->getArrayCopy()); + + $queueForRealtime + ->from($queueForEvents) + ->trigger(); + + $queueForFunctions + ->from($queueForEvents) + ->trigger(); + + $queueForWebhooks + ->from($queueForEvents) + ->trigger(); + } + + $queueForEvents->reset(); + $queueForRealtime->reset(); + $queueForFunctions->reset(); + $queueForWebhooks->reset(); + } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php index 440927d45c..f44e54f2b4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.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; @@ -72,11 +73,15 @@ class Delete 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 $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, array $queries, 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()) { @@ -134,5 +139,16 @@ class Delete extends Action 'total' => $modified, $this->getSdkGroup() => $documents, ]), $this->getResponseModel()); + + $this->triggerBulk( + 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete', + $database, + $collection, + $documents, + $queueForEvents, + $queueForRealtime, + $queueForFunctions, + $queueForWebhooks + ); } } 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 14bd71c5ea..82b39ef178 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 @@ -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; @@ -76,11 +77,15 @@ class Update 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, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $data = \is_string($data) ? \json_decode($data, true) @@ -158,5 +163,16 @@ class Update extends Action 'total' => $modified, $this->getSdkGroup() => $documents ]), $this->getResponseModel()); + + $this->triggerBulk( + 'databases.[databaseId].collections.[collectionId].documents.[documentId].update', + $database, + $collection, + $documents, + $queueForEvents, + $queueForRealtime, + $queueForFunctions, + $queueForWebhooks + ); } } 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 9e7a31c4be..0691249943 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 @@ -122,10 +122,12 @@ class Create extends Action ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->callback($this->action(...)); } - - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { $data = \is_string($data) ? \json_decode($data, true) @@ -417,6 +419,16 @@ class Create extends Action $this->getSdkGroup() => $documents ]), $this->getBulkResponseModel()); + $this->triggerBulk( + 'databases.[databaseId].collections.[collectionId].documents.[documentId].create', + $database, + $collection, + $documents, + $queueForEvents, + $queueForRealtime, + $queueForFunctions, + $queueForWebhooks + ); return; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php index 4837d30e46..9a594245b3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php @@ -59,6 +59,10 @@ class Delete extends DocumentsDelete ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php index c72d82d65d..d33e8b0fb6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php @@ -61,6 +61,10 @@ class Update extends DocumentsUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('queueForEvents') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php index 56933b882c..6fadbe1412 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php @@ -101,6 +101,9 @@ class Create extends DocumentCreate ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->callback($this->action(...)); } } diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 2ee0d0198e..c072fdca35 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -182,9 +182,9 @@ abstract class Scope extends TestCase /** * @return array */ - public function getUser(): array + public function getUser(bool $fresh = false): array { - if (isset(self::$user[$this->getProject()['$id']])) { + if (!$fresh && isset(self::$user[$this->getProject()['$id']])) { return self::$user[$this->getProject()['$id']]; } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 1844f3bce4..60f58bd8ee 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Realtime; use CURLFile; +use Exception; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -12,6 +13,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use WebSocket\ConnectionException; +use WebSocket\TimeoutException; class RealtimeCustomClientTest extends Scope { @@ -819,7 +821,6 @@ class RealtimeCustomClientTest extends Scope ]); $response = json_decode($client->receive(), true); - $this->assertArrayHasKey('type', $response); $this->assertArrayHasKey('data', $response); $this->assertEquals('event', $response['type']); @@ -898,9 +899,755 @@ class RealtimeCustomClientTest extends Scope $this->assertNotEmpty($response['data']['payload']); $this->assertEquals('Bradley Cooper', $response['data']['payload']['name']); + // test bulk create + $documents = $this->client->call(Client::METHOD_POST, "/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()), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'Scarlett Johansson', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ] + ], + ]); + + // Receive first document event + $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']}.create", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $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.*.create", $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']); + $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']); + $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']); + $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']); + + // Receive second document event + $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']}.create", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $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.*.create", $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']); + $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']); + $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']); + $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']); + + // test bulk update + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $actorsId . '/documents/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Marvel Hero', + '$permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Receive first document update event + $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']}.update", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $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.*.update", $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->assertEquals('Marvel Hero', $response['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + + // Receive second document update event + $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']}.update", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $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.*.update", $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->assertEquals('Marvel Hero', $response['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + + // Receive third document update event + $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']}.update", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $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.*.update", $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->assertEquals('Marvel Hero', $response['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response['data']['payload']); + + // Test bulk delete + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Receive first document delete event + $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']}.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $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.*.delete", $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']); + + // Receive second document delete event + $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']}.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $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.*.delete", $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']); + + // Receive third document delete event + $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']}.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $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.*.delete", $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(); } + public function testChannelDatabaseBulkOperationMultipleClient() + { + // user with api key will do operations and other valid users + $user1 = $this->getUser(true); + $user1Id = $user1['$id']; + $session = $user1['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client1 = $this->getWebsocket(['documents', 'collections'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client1->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('connected', $response['type']); + $this->assertNotEmpty($response['data']); + + $user2 = $this->getUser(true); + $user2Id = $user2['$id']; + $session = $user2['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client2 = $this->getWebsocket(['documents', 'collections'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client2->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('connected', $response['type']); + $this->assertNotEmpty($response['data']); + + + /** + * Test Database Create + */ + $database = $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' => 'Actors DB', + ]); + + $databaseId = $database['body']['$id']; + + /** + * Test Collection Create + */ + $actors = $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' => 'Actors', + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + 'documentSecurity' => true, + ]); + + $actorsId = $actors['body']['$id']; + + $name = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actorsId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $name['headers']['status-code']); + $this->assertEquals('name', $name['body']['key']); + $this->assertEquals('string', $name['body']['type']); + $this->assertEquals(256, $name['body']['size']); + $this->assertTrue($name['body']['required']); + + sleep(2); + + // create + $this->client->call(Client::METHOD_POST, "/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' => 'Any', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'Users', + '$permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'User1', + '$permissions' => [ + Permission::read(Role::user($user1Id)), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'User2', + '$permissions' => [ + Permission::read(Role::user($user2Id)), + ], + ], + [ + '$id' => ID::unique(), + 'name' => 'User2', + '$permissions' => [ + Permission::read(Role::user($user2Id)), + ], + ] + ], + ]); + + // Receive and assert for client1 - should receive 3 individual document events + for ($i = 0; $i < 3; $i++) { + $response1 = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response1); + $this->assertArrayHasKey('data', $response1); + $this->assertEquals('event', $response1['type']); + $this->assertNotEmpty($response1['data']); + $this->assertArrayHasKey('timestamp', $response1['data']); + $this->assertCount(6, $response1['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.create", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.create", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.*.collections.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']); + $this->assertContains("databases.*", $response1['data']['events']); + $this->assertNotEmpty($response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']); + $this->assertArrayHasKey('$id', $response1['data']['payload']); + $this->assertArrayHasKey('name', $response1['data']['payload']); + $this->assertArrayHasKey('$permissions', $response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']['$permissions']); + } + + // Receive and assert for client2 - should receive 4 individual document events + for ($i = 0; $i < 4; $i++) { + $response2 = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response2); + $this->assertArrayHasKey('data', $response2); + $this->assertEquals('event', $response2['type']); + $this->assertNotEmpty($response2['data']); + $this->assertArrayHasKey('timestamp', $response2['data']); + $this->assertCount(6, $response2['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.create", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.create", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.*.collections.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']); + $this->assertContains("databases.*", $response2['data']['events']); + $this->assertNotEmpty($response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']); + $this->assertArrayHasKey('$id', $response2['data']['payload']); + $this->assertArrayHasKey('name', $response2['data']['payload']); + $this->assertArrayHasKey('$permissions', $response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']['$permissions']); + } + + + // Perform bulk update(making it only accessible by user1) + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Marvel Hero', + '$permissions' => [ + Permission::read(Role::user($user1Id)), + Permission::update(Role::user($user1Id)), + Permission::delete(Role::user($user1Id)), + ] + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Receive and assert for client1 + for ($i = 0; $i < 5; $i++) { + $response1 = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response1); + $this->assertArrayHasKey('data', $response1); + $this->assertEquals('event', $response1['type']); + $this->assertNotEmpty($response1['data']); + $this->assertArrayHasKey('timestamp', $response1['data']); + $this->assertCount(6, $response1['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.*.collections.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.*", $response1['data']['events']); + $this->assertNotEmpty($response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']); + $this->assertArrayHasKey('$id', $response1['data']['payload']); + $this->assertEquals('Marvel Hero', $response1['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response1['data']['payload']); + } + + // client2 shouldn't receive any event and lead to timeout + try { + json_decode($client2->receive(), true); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + // Perform bulk update(making it only accessible by user2) + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Marvel Hero', + '$permissions' => [ + Permission::read(Role::user($user2Id)), + Permission::update(Role::user($user2Id)), + Permission::delete(Role::user($user2Id)), + ] + ], + ]); + + // Receive and assert for client2 + for ($i = 0; $i < 5; $i++) { + $response2 = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response2); + $this->assertArrayHasKey('data', $response2); + $this->assertEquals('event', $response2['type']); + $this->assertNotEmpty($response2['data']); + $this->assertArrayHasKey('timestamp', $response2['data']); + $this->assertCount(6, $response2['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.*.collections.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.*", $response2['data']['events']); + $this->assertNotEmpty($response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']); + $this->assertArrayHasKey('$id', $response2['data']['payload']); + $this->assertEquals('Marvel Hero', $response2['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response2['data']['payload']); + } + + // client1 shouldn't receive any event and lead to timeout + try { + json_decode($client1->receive(), true); + $this->fail('Expected TimeoutException was not thrown.'); + } catch (Exception $e) { + $this->assertInstanceOf(TimeoutException::class, $e); + } + + // Updating the permission for both the users + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$actorsId}/documents/", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Marvel Hero', + '$permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ] + ], + ]); + // both user1 and user2 should receive the event + for ($i = 0; $i < 5; $i++) { + $response1 = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response1); + $this->assertArrayHasKey('data', $response1); + $this->assertEquals('event', $response1['type']); + $this->assertNotEmpty($response1['data']); + $this->assertArrayHasKey('timestamp', $response1['data']); + $this->assertCount(6, $response1['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.*.collections.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); + $this->assertContains("databases.*", $response1['data']['events']); + $this->assertNotEmpty($response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']); + $this->assertArrayHasKey('$id', $response1['data']['payload']); + $this->assertEquals('Marvel Hero', $response1['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response1['data']['payload']); + + $response2 = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response2); + $this->assertArrayHasKey('data', $response2); + $this->assertEquals('event', $response2['type']); + $this->assertNotEmpty($response2['data']); + $this->assertArrayHasKey('timestamp', $response2['data']); + $this->assertCount(6, $response2['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.*.collections.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); + $this->assertContains("databases.*", $response2['data']['events']); + $this->assertNotEmpty($response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']); + $this->assertArrayHasKey('$id', $response2['data']['payload']); + $this->assertEquals('Marvel Hero', $response2['data']['payload']['name']); + $this->assertArrayHasKey('$permissions', $response2['data']['payload']); + } + + // Perform bulk delete + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$actorsId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Receive and assert for client1 + for ($i = 0; $i < 5; $i++) { + $response1 = json_decode($client1->receive(), true); + $this->assertArrayHasKey('type', $response1); + $this->assertArrayHasKey('data', $response1); + $this->assertEquals('event', $response1['type']); + $this->assertNotEmpty($response1['data']); + $this->assertArrayHasKey('timestamp', $response1['data']); + $this->assertCount(6, $response1['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.delete", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.delete", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.*.collections.*", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response1['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response1['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']); + $this->assertContains("databases.*", $response1['data']['events']); + $this->assertNotEmpty($response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']); + $this->assertArrayHasKey('$id', $response1['data']['payload']); + $this->assertArrayHasKey('name', $response1['data']['payload']); + $this->assertArrayHasKey('$permissions', $response1['data']['payload']); + $this->assertIsArray($response1['data']['payload']['$permissions']); + } + + // Receive and assert for client2 + for ($i = 0; $i < 5; $i++) { + $response2 = json_decode($client2->receive(), true); + $this->assertArrayHasKey('type', $response2); + $this->assertArrayHasKey('data', $response2); + $this->assertEquals('event', $response2['type']); + $this->assertNotEmpty($response2['data']); + $this->assertArrayHasKey('timestamp', $response2['data']); + $this->assertCount(6, $response2['data']['channels']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.delete", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*.delete", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.*.collections.*", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*", $response2['data']['events']); + $this->assertContains("databases.*.collections.{$actorsId}", $response2['data']['events']); + $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']); + $this->assertContains("databases.*", $response2['data']['events']); + $this->assertNotEmpty($response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']); + $this->assertArrayHasKey('$id', $response2['data']['payload']); + $this->assertArrayHasKey('name', $response2['data']['payload']); + $this->assertArrayHasKey('$permissions', $response2['data']['payload']); + $this->assertIsArray($response2['data']['payload']['$permissions']); + } + + $client1->close(); + $client2->close(); + } + public function testChannelDatabaseCollectionPermissions() { $user = $this->getUser();