mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge pull request #10626 from appwrite/feat-txn
Fix cross API compatibility
This commit is contained in:
commit
821b3b86e4
10 changed files with 497 additions and 33 deletions
|
|
@ -205,6 +205,31 @@ abstract class Action extends AppwriteAction
|
|||
return $this->isCollectionsAPI() ? 'collection' : 'table';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct attribute/column key for increment/decrement operations.
|
||||
*/
|
||||
protected function getAttributeKey(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'attribute' : 'column';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key used in ID parameters (e.g., 'collectionId' or 'tableId').
|
||||
*/
|
||||
protected function getGroupId(): string
|
||||
{
|
||||
return $this->getCollectionsEventsContext() . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource ID key for the current action.
|
||||
*/
|
||||
protected function getResourceId(): string
|
||||
{
|
||||
$resource = $this->isCollectionsAPI() ? 'document' : 'row';
|
||||
return $resource . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove configured removable attributes from a document.
|
||||
* Used for relationship path handling to remove API-specific attributes.
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class Decrement extends Action
|
|||
'documentId' => $documentId,
|
||||
'action' => 'decrement',
|
||||
'data' => [
|
||||
'attribute' => $attribute,
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'min' => $min,
|
||||
],
|
||||
|
|
@ -153,9 +153,10 @@ class Decrement extends Action
|
|||
});
|
||||
|
||||
// Return successful response without actually decrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class Increment extends Action
|
|||
'documentId' => $documentId,
|
||||
'action' => 'increment',
|
||||
'data' => [
|
||||
'attribute' => $attribute,
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'max' => $max,
|
||||
],
|
||||
|
|
@ -153,9 +153,10 @@ class Increment extends Action
|
|||
});
|
||||
|
||||
// Return successful response without actually incrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -419,9 +419,10 @@ class Create extends Action
|
|||
'total' => \count($documents),
|
||||
]), $this->getBulkResponseModel());
|
||||
} else {
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documents[0]['$id'] ?? $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$documents[0]
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -292,9 +292,10 @@ class Update extends Action
|
|||
});
|
||||
|
||||
// Return successful response without actually updating document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$document->getArrayCopy(),
|
||||
...$data
|
||||
|
|
|
|||
|
|
@ -301,9 +301,10 @@ class Upsert extends Action
|
|||
});
|
||||
|
||||
// Return successful response without actually upserting document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$data
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -167,8 +167,10 @@ class Update extends Action
|
|||
}
|
||||
}
|
||||
|
||||
$totalOperations++;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||
if (!\in_array($action, ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$totalOperations++;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if ($data instanceof Document) {
|
||||
$data = $data->getArrayCopy();
|
||||
|
|
@ -194,16 +196,24 @@ class Update extends Action
|
|||
$this->handleDecrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'bulkCreate':
|
||||
$this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpdate':
|
||||
$this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpsert':
|
||||
$this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkDelete':
|
||||
$this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -547,6 +557,28 @@ class Update extends Action
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attribute/column name from data, with fallback for cross-API compatibility
|
||||
*
|
||||
* @param array $data The operation data
|
||||
* @return string The attribute/column name
|
||||
*/
|
||||
private function getAttributeNameFromData(array $data): string
|
||||
{
|
||||
$expectedKey = $this->getAttributeKey();
|
||||
if (isset($data[$expectedKey])) {
|
||||
return $data[$expectedKey];
|
||||
}
|
||||
|
||||
// Try the opposite key for cross-API compatibility
|
||||
$fallbackKey = $expectedKey === 'attribute' ? 'column' : 'attribute';
|
||||
if (isset($data[$fallbackKey])) {
|
||||
return $data[$fallbackKey];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle increment operation
|
||||
*
|
||||
|
|
@ -569,23 +601,24 @@ class Update extends Action
|
|||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
|
|
@ -614,23 +647,24 @@ class Update extends Action
|
|||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
|
|
@ -645,7 +679,7 @@ class Update extends Action
|
|||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents created
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleBulkCreateOperation(
|
||||
|
|
@ -654,13 +688,14 @@ class Update extends Action
|
|||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) {
|
||||
): int {
|
||||
$count = 0;
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state, &$count) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$dbForProject->createDocuments(
|
||||
$count = $dbForProject->createDocuments(
|
||||
$collectionId,
|
||||
$documents,
|
||||
onNext: function (Document $document) use (&$state, $collectionId) {
|
||||
|
|
@ -668,6 +703,7 @@ class Update extends Action
|
|||
}
|
||||
);
|
||||
});
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -679,7 +715,7 @@ class Update extends Action
|
|||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents updated
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
|
|
@ -691,7 +727,7 @@ class Update extends Action
|
|||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$updateData = new Document($data['data']);
|
||||
|
||||
|
|
@ -701,7 +737,7 @@ class Update extends Action
|
|||
|
||||
// Clone the document before passing to updateDocuments to prevent mutation
|
||||
// The database layer mutates the input document, which would corrupt transaction state
|
||||
$dbForProject->updateDocuments(
|
||||
$count = $dbForProject->updateDocuments(
|
||||
$collectionId,
|
||||
clone $updateData,
|
||||
$queries,
|
||||
|
|
@ -739,6 +775,8 @@ class Update extends Action
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -750,7 +788,7 @@ class Update extends Action
|
|||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents upserted
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
|
|
@ -761,14 +799,14 @@ class Update extends Action
|
|||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$mergedDocuments = $transactionState->applyBulkUpsertToState($collectionId, $documents, $state);
|
||||
|
||||
$dbForProject->upsertDocuments(
|
||||
$count = $dbForProject->upsertDocuments(
|
||||
$collectionId,
|
||||
$mergedDocuments,
|
||||
onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
|
|
@ -786,6 +824,8 @@ class Update extends Action
|
|||
$state[$collectionId][$upserted->getId()] = $upserted;
|
||||
}
|
||||
);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -797,7 +837,7 @@ class Update extends Action
|
|||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents deleted
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
|
|
@ -809,10 +849,10 @@ class Update extends Action
|
|||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
|
||||
$dbForProject->deleteDocuments(
|
||||
$count = $dbForProject->deleteDocuments(
|
||||
$collectionId,
|
||||
$queries,
|
||||
onNext: function (Document $deleted, Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
|
|
@ -832,5 +872,7 @@ class Update extends Action
|
|||
);
|
||||
|
||||
$transactionState->applyBulkDeleteToState($collectionId, $queries, $state);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ class Document extends Any
|
|||
{
|
||||
$document->removeAttribute('$collection');
|
||||
$document->removeAttribute('$tenant');
|
||||
$document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0));
|
||||
|
||||
if (!$document->isEmpty()) {
|
||||
$document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0));
|
||||
}
|
||||
|
||||
foreach ($document->getAttributes() as $attribute) {
|
||||
if (\is_array($attribute)) {
|
||||
|
|
|
|||
|
|
@ -3732,6 +3732,149 @@ trait TransactionsBase
|
|||
$this->assertEquals(0, $doc['body']['score']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test individual increment/decrement endpoints with transactions for Legacy Collections API
|
||||
* This test ensures that:
|
||||
* 1. Transaction logs store the correct attribute key ('attribute' for Collections API)
|
||||
* 2. Mock responses return the correct ID keys ('$collectionId' not '$tableId')
|
||||
*/
|
||||
public function testIncrementDecrementEndpointsWithTransaction(): 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' => 'IncrDecrEndpointTestDB'
|
||||
]);
|
||||
|
||||
$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' => 'AccountsCollection',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Add balance attribute
|
||||
$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' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial documents
|
||||
$this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => 'joe',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => 'jane',
|
||||
'data' => ['balance' => 50]
|
||||
]);
|
||||
|
||||
// Create transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(201, $transaction['headers']['status-code']);
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Test: Decrement using individual endpoint - should store 'attribute' not 'column' in transaction log
|
||||
$decrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/{$databaseId}/collections/{$collectionId}/documents/joe/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
'min' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
// Test: Response should return '$collectionId' not '$tableId' for Collections API
|
||||
$this->assertEquals(200, $decrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$collectionId', $decrementResponse['body'], 'Response should contain $collectionId for Collections API');
|
||||
$this->assertArrayNotHasKey('$tableId', $decrementResponse['body'], 'Response should not contain $tableId for Collections API');
|
||||
$this->assertEquals($collectionId, $decrementResponse['body']['$collectionId']);
|
||||
|
||||
// Test increment endpoint
|
||||
$incrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/{$databaseId}/collections/{$collectionId}/documents/jane/balance/increment",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $incrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$collectionId', $incrementResponse['body'], 'Response should contain $collectionId for Collections API');
|
||||
$this->assertArrayNotHasKey('$tableId', $incrementResponse['body'], 'Response should not contain $tableId for Collections API');
|
||||
$this->assertEquals($collectionId, $incrementResponse['body']['$collectionId']);
|
||||
|
||||
// Commit transaction - this will fail if transaction log has 'column' instead of 'attribute'
|
||||
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'commit' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Transaction commit should succeed');
|
||||
|
||||
// Verify final values
|
||||
$joe = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/joe", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$jane = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/jane", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $joe['headers']['status-code']);
|
||||
$this->assertEquals(50, $joe['body']['balance'], 'Joe should have 100 - 50 = 50');
|
||||
|
||||
$this->assertEquals(200, $jane['headers']['status-code']);
|
||||
$this->assertEquals(100, $jane['body']['balance'], 'Jane should have 50 + 50 = 100');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bulk update operations in transaction
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3867,6 +3867,252 @@ trait TransactionsBase
|
|||
$this->assertEquals('updated', $row['body']['status'], 'Status should be updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test individual increment/decrement endpoints with transactions
|
||||
* This test ensures that:
|
||||
* 1. Transaction logs store the correct attribute key ('column' for TablesDB)
|
||||
* 2. Mock responses return the correct ID keys ('$tableId' not '$collectionId')
|
||||
*/
|
||||
public function testIncrementDecrementEndpointsWithTransaction(): void
|
||||
{
|
||||
// Create database and table
|
||||
$database = $this->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' => 'IncrDecrEndpointTestDB'
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'AccountsTable',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
// Add balance column
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial rows
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'joe',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'jane',
|
||||
'data' => ['balance' => 50]
|
||||
]);
|
||||
|
||||
// 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']);
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Test Bug 1: Decrement using individual endpoint - should store 'column' not 'attribute' in transaction log
|
||||
$decrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
'min' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
// Test Bug 2: Response should return '$tableId' not '$collectionId'
|
||||
$this->assertEquals(200, $decrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$tableId', $decrementResponse['body'], 'Response should contain $tableId for TablesDB API');
|
||||
$this->assertArrayNotHasKey('$collectionId', $decrementResponse['body'], 'Response should not contain $collectionId for TablesDB API');
|
||||
$this->assertEquals($tableId, $decrementResponse['body']['$tableId']);
|
||||
|
||||
// Test increment endpoint
|
||||
$incrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane/balance/increment",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $incrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$tableId', $incrementResponse['body'], 'Response should contain $tableId for TablesDB API');
|
||||
$this->assertArrayNotHasKey('$collectionId', $incrementResponse['body'], 'Response should not contain $collectionId for TablesDB API');
|
||||
$this->assertEquals($tableId, $incrementResponse['body']['$tableId']);
|
||||
|
||||
// Commit transaction - this will fail if transaction log has 'attribute' instead of 'column'
|
||||
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'commit' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Transaction commit should succeed');
|
||||
|
||||
// Verify final values
|
||||
$joe = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$jane = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $joe['headers']['status-code']);
|
||||
$this->assertEquals(50, $joe['body']['balance'], 'Joe should have 100 - 50 = 50');
|
||||
|
||||
$this->assertEquals(200, $jane['headers']['status-code']);
|
||||
$this->assertEquals(100, $jane['body']['balance'], 'Jane should have 50 + 50 = 100');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cross-API compatibility: stage operations via TablesDB, commit via Collections API
|
||||
* This ensures fallback logic works when APIs are mixed
|
||||
*/
|
||||
public function testCrossAPIIncrementDecrement(): void
|
||||
{
|
||||
// Create database and table
|
||||
$database = $this->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' => 'CrossAPITestDB'
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'CrossAPITable',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
// Add balance column
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial row
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'test',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
// Create transaction using TablesDB API
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Stage operations using TablesDB API (will store 'column' key)
|
||||
$this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/test/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 30,
|
||||
]
|
||||
);
|
||||
|
||||
// Commit using Collections API (expects 'attribute' key but should fallback to 'column')
|
||||
$commitResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/transactions/{$transactionId}",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'commit' => true
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Cross-API commit should succeed');
|
||||
|
||||
// Verify final value
|
||||
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $row['headers']['status-code']);
|
||||
$this->assertEquals(70, $row['body']['balance'], 'Balance should be 100 - 30 = 70');
|
||||
}
|
||||
|
||||
public function testBulkUpdateWithDependentDocuments(): void
|
||||
{
|
||||
// Create database and table
|
||||
|
|
|
|||
Loading…
Reference in a new issue