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 b57c047a8c..82f940bb62 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 @@ -104,6 +104,8 @@ class Delete extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + $originalQueries = $queries; + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -135,7 +137,7 @@ class Delete extends Action 'transactionInternalId' => $transaction->getSequence(), 'action' => 'bulkDelete', 'data' => [ - 'queries' => $queries, + 'queries' => $originalQueries, ], ]); 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 41b8bbda74..1b9559d4b9 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 @@ -116,13 +116,15 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + $originalQueries = $queries; + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - if ($data['$permissions']) { + if (isset($data['$permissions'])) { $validator = new Permissions(); if (!$validator->isValid($data['$permissions'])) { throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); @@ -157,7 +159,7 @@ class Update extends Action 'action' => 'bulkUpdate', 'data' => [ 'data' => $data, - 'queries' => $queries, + 'queries' => $originalQueries, ], ]); @@ -167,7 +169,6 @@ class Update extends Action 'transactions', $transactionId, 'operations', - 1 ); }); diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php index f1e8b64d58..9409834be0 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -19,7 +19,7 @@ class TransactionsTest extends Scope /** * Test creating a transaction */ - public function testCreate(): array + public function testCreate(): void { // Create database first $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -91,21 +91,35 @@ class TransactionsTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - - return [ - 'databaseId' => $databaseId, - 'transactionId1' => $transactionId1, - 'transactionId2' => $transactionId2 - ]; } /** - * @depends testCreate + * Test adding operations to a transaction */ - public function testAddOperations(array $data): array + public function testAddOperations(): void { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId1']; + // Create database first + $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' => 'TransactionOperationsTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; // Create a collection for testing $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ @@ -233,20 +247,109 @@ class TransactionsTest extends Scope ]); $this->assertEquals(404, $response['headers']['status-code']); - - return array_merge($data, [ - 'collectionId' => $collectionId - ]); } /** - * @depends testAddOperations + * Test committing a transaction */ - public function testCommit(array $data): void + public function testCommit(): void { - $databaseId = $data['databaseId']; - $collectionId = $data['collectionId']; - $transactionId = $data['transactionId1']; + // Create database first + $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' => 'TransactionCommitTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionCommitTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add attributes + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + 'name' => 'Test Document 2' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); // Commit the transaction $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ @@ -292,12 +395,32 @@ class TransactionsTest extends Scope } /** - * @depends testCreate + * Test rolling back a transaction */ - public function testRollback(array $data): void + public function testRollback(): void { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId2']; + // Create database first + $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' => 'TransactionRollbackTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; // Create a collection for rollback test $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ @@ -1384,4 +1507,1453 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); // Document not found } + + /** + * Test createDocument with transactionId via normal route + */ + public function testCreateDocument(): void + { + // Create database and collection + $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' => 'WriteRoutesTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $attributes = [ + ['key' => 'name', 'type' => 'string', 'size' => 256, 'required' => true], + ['key' => 'counter', 'type' => 'integer', 'required' => false, 'min' => 0, 'max' => 10000], + ['key' => 'category', 'type' => 'string', 'size' => 256, 'required' => false], + ['key' => 'data', 'type' => 'string', 'size' => 256, 'required' => false], + ]; + + foreach ($attributes as $attr) { + $type = $attr['type']; + unset($attr['type']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/{$type}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $attr); + + $this->assertEquals(202, $response['headers']['status-code']); + } + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Create document via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_from_route', + 'data' => [ + 'name' => 'Created via normal route', + 'counter' => 100, + 'category' => 'test' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Created via normal route', $response['body']['name']); + } + + /** + * Test updateDocument with transactionId via normal route + */ + public function testUpdateDocument(): void + { + // Create database and collection + $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' => 'UpdateRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_update', + 'data' => [ + 'name' => 'Original name', + 'counter' => 50, + 'category' => 'original' + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Update document via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Updated via normal route', + 'counter' => 150, + 'category' => 'updated' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should still have original values outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Original name', $response['body']['name']); + $this->assertEquals(50, $response['body']['counter']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now have updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated via normal route', $response['body']['name']); + $this->assertEquals(150, $response['body']['counter']); + } + + /** + * Test upsertDocument with transactionId via normal route + */ + public function testUpsertDocument(): void + { + // Create database and collection + $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' => 'UpsertRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Upsert document (create) via normal route with transactionId + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Created by upsert', + 'counter' => 25 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Upsert same document (update) in same transaction + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Updated by upsert', + 'counter' => 75 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); // Upsert in transaction returns 201 + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Updated by upsert', $response['body']['name']); + $this->assertEquals(75, $response['body']['counter']); + } + + /** + * Test deleteDocument with transactionId via normal route + */ + public function testDeleteDocument(): void + { + // Create database and collection + $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' => 'DeleteRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_delete', + 'data' => ['name' => 'Will be deleted'] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Delete document via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Document should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should no longer exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + /** + * Test bulkCreate with transactionId via normal route + */ + public function testBulkCreate(): void + { + // Create database and collection + $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' => 'BulkCreateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk create via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_create_1', + 'name' => 'Bulk created 1', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_2', + 'name' => 'Bulk created 2', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_3', + 'name' => 'Bulk created 3', + 'category' => 'bulk_created' + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk operations return 200 + + // Documents should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + + // Individual document check + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_create_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Verify individual documents + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_create_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("Bulk created {$i}", $response['body']['name']); + $this->assertEquals('bulk_created', $response['body']['category']); + } + } + + /** + * Test bulkUpdate with transactionId via normal route + */ + public function testBulkUpdate(): void + { + // Create database and collection + $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' => 'BulkUpdateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_update_' . $i, + 'data' => [ + 'name' => 'Bulk doc ' . $i, + 'category' => 'bulk_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk update via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()], + 'data' => ['category' => 'bulk_updated'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should still have original category outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now have updated category + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_updated'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + } + + /** + * Test bulkUpsert with transactionId via normal route + */ + public function testBulkUpsert(): void + { + // Create database and collection + $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' => 'BulkUpsertTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create one document outside transaction + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_upsert_existing', + 'data' => [ + 'name' => 'Existing doc', + 'counter' => 10 + ] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk upsert via normal route with transactionId (updates existing, creates new) + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_upsert_existing', + 'name' => 'Updated existing', + 'counter' => 20 + ], + [ + '$id' => 'bulk_upsert_new', + 'name' => 'New doc', + 'counter' => 30 + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Original document should be unchanged, new document shouldn't exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Existing doc', $response['body']['name']); + $this->assertEquals(10, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Check both documents exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated existing', $response['body']['name']); + $this->assertEquals(20, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('New doc', $response['body']['name']); + $this->assertEquals(30, $response['body']['counter']); + } + + /** + * Test bulkDelete with transactionId via normal route + */ + public function testBulkDelete(): void + { + // Create database and collection + $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' => 'BulkDeleteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_delete_' . $i, + 'data' => [ + 'name' => 'Delete doc ' . $i, + 'category' => 'bulk_delete_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk delete via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk delete with transaction returns 200 + + // Documents should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now be deleted + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + } + + /** + * Test multiple single route operations in one transaction + */ + public function testMixedSingleOperations(): void + { + // Create database and collection + $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' => 'MultipleSingleRoutesDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'required' => false, + 'min' => 1, + 'max' => 10, + ]); + + sleep(3); + + // Create an existing document outside transaction for testing + $existingDoc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_doc', + 'data' => [ + 'name' => 'Existing Document', + 'status' => 'active', + 'priority' => 5 + ] + ]); + + $this->assertEquals(201, $existingDoc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + $this->assertEquals(201, $transaction['headers']['status-code']); + + // 1. Create new document via normal route with transactionId + $response1 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_1', + 'data' => [ + 'name' => 'New Document 1', + 'status' => 'pending', + 'priority' => 1 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response1['headers']['status-code']); + + // 2. Create another document via normal route with transactionId + $response2 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_2', + 'data' => [ + 'name' => 'New Document 2', + 'status' => 'pending', + 'priority' => 2 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response2['headers']['status-code']); + + // 3. Update existing document via normal route with transactionId + $response3 = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'updated', + 'priority' => 10 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response3['headers']['status-code']); + + // 4. Update the first new document (created in same transaction) + $response4 = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'active', + 'priority' => 8 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response4['headers']['status-code']); + + // 5. Delete the second new document (created in same transaction) + $response5 = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response5['headers']['status-code']); + + // 6. Upsert a new document via normal route with transactionId + $response6 = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'upserted_doc', + 'data' => [ + 'name' => 'Upserted Document', + 'status' => 'new', + 'priority' => 3 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response6['headers']['status-code']); + + // Check transaction has correct number of operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $txnDetails['headers']['status-code']); + $this->assertEquals(6, $txnDetails['body']['operations']); // 6 operations total + + // Verify nothing exists outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Existing doc should still have original values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(5, $response['body']['priority']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify final state after commit + // new_doc_1 should exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('New Document 1', $response['body']['name']); + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(8, $response['body']['priority']); + + // new_doc_2 should not exist (was deleted in transaction) + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // existing_doc should have updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('updated', $response['body']['status']); + $this->assertEquals(10, $response['body']['priority']); + + // upserted_doc should exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Upserted Document', $response['body']['name']); + $this->assertEquals('new', $response['body']['status']); + $this->assertEquals(3, $response['body']['priority']); + + // Verify total document count + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(3, $documents['body']['total']); // existing_doc, new_doc_1, upserted_doc + } + + /** + * Test mixed operations with transactions + */ + public function testMixedOperations(): void + { + // Create database and collection + $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' => 'MixedOpsTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operation via Operations\Add endpoint + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'mixed_doc1', + 'data' => ['name' => 'Via Operations Add'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['operations']); + + // Add operation via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'mixed_doc2', + 'data' => ['name' => 'Via normal route'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Check transaction now has 2 operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(2, $txnDetails['body']['operations']); + + // Both documents shouldn't exist yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Both documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via Operations Add', $response['body']['name']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via normal route', $response['body']['name']); + } }