client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'databaseId' => ID::unique(), 'name' => 'PermissionsTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $this->permissionsDatabase = $database['body']['$id']; } /** * Test collection-level create permission check on staging */ public function testCollectionCreatePermissionDenied(): void { // Create a collection with no create permission for current user $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest1', 'name' => 'Permission Test 1', 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to stage a create operation without permission, should fail $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'create', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc1', 'data' => ['title' => 'Test Document'], ]] ]); // This should fail with 401 Unauthorized if ($staged['headers']['status-code'] !== 401) { echo "\nDEBUG - Actual response code: " . $staged['headers']['status-code'] . "\n"; echo "DEBUG - Response body: " . json_encode($staged['body'], JSON_PRETTY_PRINT) . "\n"; } $this->assertEquals(401, $staged['headers']['status-code']); } /** * Test collection-level update permission check on staging */ public function testCollectionUpdatePermissionDenied(): void { // Create a collection with create but no update permission $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest2', 'name' => 'Permission Test 2', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create a document first with API key $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'testDoc2', 'data' => ['title' => 'Original Title'], ]); $this->assertEquals(201, $doc['headers']['status-code']); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to stage an update operation without permission, should fail $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'update', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc2', 'data' => ['title' => 'Updated Title'], ]] ]); // This should fail with 401 Unauthorized $this->assertEquals(401, $staged['headers']['status-code']); } /** * Test collection-level delete permission check on staging */ public function testCollectionDeletePermissionDenied(): void { // Create a collection with create, read but no delete permission $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest3', 'name' => 'Permission Test 3', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'testDoc3', 'data' => ['title' => 'To Be Deleted'], ]); $this->assertEquals(201, $doc['headers']['status-code']); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to stage a delete operation without permission, should fail $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'delete', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc3', 'data' => [], ]] ]); // This should fail with 401 Unauthorized $this->assertEquals(401, $staged['headers']['status-code']); } /** * Test document-level update permission grants access when rowSecurity is enabled * Collection has no update permission, but document does, should succeed */ public function testDocumentLevelUpdatePermissionGranted(): void { // Create collection with rowSecurity enabled but no update permission at collection level $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest4', 'name' => 'Permission Test 4', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), ], 'rowSecurity' => true, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create a document with update permission at document level $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'testDoc4', 'data' => ['title' => 'Protected Document'], 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $this->assertEquals(201, $doc['headers']['status-code']); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Stage an update, should succeed because document has update permission $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'update', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc4', 'data' => ['title' => 'Trying to Update'], ]] ]); // This should succeed with 201 because document has update permission $this->assertEquals(201, $staged['headers']['status-code']); } /** * Test document-level delete permission grants access when rowSecurity is enabled * Collection has no delete permission, but document does, should succeed */ public function testDocumentLevelDeletePermissionGranted(): void { // Create collection with rowSecurity enabled but no delete permission at collection level $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest5', 'name' => 'Permission Test 5', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], 'rowSecurity' => true, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create a document with delete permission at document level $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'testDoc5', 'data' => ['title' => 'Can Delete Me'], 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $doc['headers']['status-code']); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Stage a delete should succeed because document has delete permission $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'delete', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc5', 'data' => [], ]] ]); // This should succeed with 201 because document has DELETE permission $this->assertEquals(201, $staged['headers']['status-code']); } /** * Test that users cannot set permissions for roles they don't have */ public function testCannotSetUnauthorizedRolePermissions(): void { // Create a collection $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest6', 'name' => 'Permission Test 6', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => true, ]); $this->assertEquals(201, $collection['headers']['status-code']); // Add attribute $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to stage a create with team permissions, current user is not in team $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'create', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc6', 'data' => [ 'title' => 'Admin Only Doc', '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::team('adminTeam')), ], ], ]] ]); // This should fail with 401 Unauthorized, cannot set permissions for roles you don't have $this->assertEquals(401, $staged['headers']['status-code']); $this->assertStringContainsString('Permissions must be one of', $staged['body']['message']); } /** * Test successful staging when user has the required permissions */ public function testSuccessfulStagingWithProperPermissions(): void { $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest7', 'name' => 'Permission Test 7', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => true, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Stage a create with permissions for current user's roles, should succeed $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'create', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'testDoc7', 'data' => [ 'title' => 'Valid Document', '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::user($this->getUser()['$id'])), ], ], ]] ]); // This should succeed $this->assertEquals(201, $staged['headers']['status-code']); $this->assertEquals(1, $staged['body']['operations']); } /** * Test that non-existent documents cannot be updated in transactions */ public function testCannotUpdateNonExistentDocument(): void { // Create a collection $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest8', 'name' => 'Permission Test 8', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to update a document that doesn't exist - should fail $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'update', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'nonExistentDoc', 'data' => ['title' => 'Trying to Update'], ]] ]); // This should fail with 404 Not Found $this->assertEquals(404, $staged['headers']['status-code']); } /** * Test that non-existent documents cannot be deleted in transactions */ public function testCannotDeleteNonExistentDocument(): void { // Create a collection $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest9', 'name' => 'Permission Test 9', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Try to delete a document that doesn't exist, should fail $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'delete', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'nonExistentDoc', 'data' => [], ]] ]); // This should fail with 404 Not Found $this->assertEquals(404, $staged['headers']['status-code']); } /** * Test that a document created in one batch can be updated in a subsequent batch within the same transaction * This validates the transactionState->getDocument() fix for cross-batch dependencies */ public function testCanUpdateDocumentCreatedInPreviousBatch(): void { $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'tableId' => 'permTest10', 'name' => 'Permission Test 10', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'rowSecurity' => false, ]); $this->assertEquals(201, $collection['headers']['status-code']); $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'title', 'size' => 255, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); // Batch 1: Create a document $batch1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'create', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'crossBatchDoc', 'data' => [ 'title' => 'Initial Title', ], ]] ]); $this->assertEquals(201, $batch1['headers']['status-code']); $this->assertEquals(1, $batch1['body']['operations']); // Batch 2: Update the document created in batch 1 $batch2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'update', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'crossBatchDoc', 'data' => [ 'title' => 'Updated Title', ], ]] ]); // This should succeed with 201 because transactionState finds the staged document from batch 1 $this->assertEquals(201, $batch2['headers']['status-code']); $this->assertEquals(2, $batch2['body']['operations']); // Batch 3: Delete the same document $batch3 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [[ 'action' => 'delete', 'databaseId' => $this->permissionsDatabase, 'tableId' => $collection['body']['$id'], 'rowId' => 'crossBatchDoc', 'data' => [], ]] ]); // This should also succeed with 201 $this->assertEquals(201, $batch3['headers']['status-code']); $this->assertEquals(3, $batch3['body']['operations']); // Rollback to clean up $rollback = $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transaction['body']['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'rollback' => true, ]); $this->assertEquals(200, $rollback['headers']['status-code']); } }