diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index f71ed4f187..1a6a7477af 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -13,10 +13,12 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Type as TypeException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -85,12 +87,12 @@ class Decrement extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + $collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); if ($collection->isEmpty()) { throw new Exception($this->getParentNotFoundException()); } @@ -142,7 +144,7 @@ class Decrement extends Action '$id' => $documentId, '$collectionId' => $collectionId, '$databaseId' => $databaseId, - $attribute => $value, // Mock response - actual value would be computed during commit + $attribute => $value, ]); $response ->setStatusCode(SwooleResponse::STATUS_CODE_OK) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index d9d9beefa7..b4246377ca 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -13,10 +13,12 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Type as TypeException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -142,7 +144,7 @@ class Increment extends Action '$id' => $documentId, '$collectionId' => $collectionId, '$databaseId' => $databaseId, - $attribute => $value, // Mock response - actual value would be computed during commit + $attribute => $value, ]); $response ->setStatusCode(SwooleResponse::STATUS_CODE_OK) 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 da919a64bf..fedbf7d5b5 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 @@ -133,7 +133,6 @@ class Delete extends Action 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => null, // Bulk operation doesn't have specific document ID 'action' => 'bulkDelete', 'data' => [ 'queries' => $queries, @@ -146,7 +145,6 @@ class Delete extends Action 'transactions', $transactionId, 'operations', - 1 ); }); 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 fdc8efdd83..caaeaced1a 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 @@ -122,6 +122,13 @@ class Update extends Action throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } + if ($data['$permissions']) { + $validator = new Permissions(); + if (!$validator->isValid($data['$permissions'])) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); + } + } + // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); @@ -145,7 +152,6 @@ class Update extends Action 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => null, // Bulk operation doesn't have specific document ID 'action' => 'bulkUpdate', 'data' => [ 'data' => $data, @@ -171,13 +177,6 @@ class Update extends Action return; } - if ($data['$permissions']) { - $validator = new Permissions(); - if (!$validator->isValid($data['$permissions'])) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); - } - } - $documents = []; try { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index e3cbe56752..b3288cb4fb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -101,6 +101,10 @@ class Upsert extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + foreach ($documents as $key => $document) { + $documents[$key] = new Document($document); + } + // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); @@ -111,7 +115,7 @@ class Upsert extends Action // Enforce max operations per transaction $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); - if (($existing + \count($documents)) > $maxBatch) { + if (($existing + 1) > $maxBatch) { throw new Exception( Exception::TRANSACTION_LIMIT_EXCEEDED, 'Transaction already has ' . $existing . ' operations, adding ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch @@ -119,21 +123,17 @@ class Upsert extends Action } // Stage the operations in transaction logs - $staged = []; - foreach ($documents as $document) { - $staged[] = new Document([ - '$id' => ID::unique(), - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $document['$id'] ?? ID::unique(), - 'action' => 'upsert', - 'data' => $document, - ]); - } + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'bulkUpsert', + 'data' => $documents, + ]); $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { - $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->createDocument('transactionLogs', $staged); $dbForProject->increaseDocumentAttribute( 'transactions', $transactionId, @@ -147,11 +147,8 @@ class Upsert extends Action $this->getSdkGroup() => [], 'total' => \count($documents), ]), $this->getResponseModel()); - return; - } - foreach ($documents as $key => $document) { - $documents[$key] = new Document($document); + return; } $upserted = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index 5ae817dbed..0332ca3673 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -130,61 +130,6 @@ class Update extends Action throw new Exception($this->getNotFoundException()); } - // Handle transaction staging - if ($transactionId !== null) { - $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty()) { - throw new Exception(Exception::TRANSACTION_NOT_FOUND); - } - if ($transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::TRANSACTION_NOT_READY); - } - - // Enforce max operations per transaction - $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; - $existing = $transaction->getAttribute('operations', 0); - if (($existing + 1) > $maxBatch) { - throw new Exception( - Exception::TRANSACTION_LIMIT_EXCEEDED, - 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch - ); - } - - // Stage the operation in transaction logs - $staged = new Document([ - '$id' => ID::unique(), - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $documentId, - 'action' => 'update', - 'data' => $data, - ]); - - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { - $dbForProject->createDocument('transactionLogs', $staged); - $dbForProject->increaseDocumentAttribute( - 'transactions', - $transactionId, - 'operations', - 1 - ); - }); - - // Return successful response without actually updating document - $mockDocument = new Document([ - '$id' => $documentId, - '$collectionId' => $collectionId, - '$databaseId' => $databaseId, - ...$document->getArrayCopy(), - ...$data - ]); - $response - ->setStatusCode(SwooleResponse::STATUS_CODE_OK) - ->dynamic($mockDocument, $this->getResponseModel()); - return; - } - // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions, [ Database::PERMISSION_READ, @@ -298,6 +243,63 @@ class Update extends Action ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations); + + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); + } + + // Enforce max operations per transaction + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; + $existing = $transaction->getAttribute('operations', 0); + if (($existing + 1) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch + ); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'update', + 'data' => $data, + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually updating document + $mockDocument = new Document([ + '$id' => $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + ...$document->getArrayCopy(), + ...$data + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($mockDocument, $this->getResponseModel()); + return; + } + + try { $document = $dbForProject->withRequestTimestamp( $requestTimestamp, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index e1a4153518..316fe4f484 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -112,60 +112,6 @@ class Upsert extends Action throw new Exception($this->getParentNotFoundException()); } - // Handle transaction staging - if ($transactionId !== null) { - $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty()) { - throw new Exception(Exception::TRANSACTION_NOT_FOUND); - } - if ($transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::TRANSACTION_NOT_READY); - } - - // Enforce max operations per transaction - $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; - $existing = $transaction->getAttribute('operations', 0); - if (($existing + 1) > $maxBatch) { - throw new Exception( - Exception::TRANSACTION_LIMIT_EXCEEDED, - 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch - ); - } - - // Stage the operation in transaction logs - $staged = new Document([ - '$id' => ID::unique(), - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $documentId, - 'action' => 'upsert', - 'data' => $data, - ]); - - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { - $dbForProject->createDocument('transactionLogs', $staged); - $dbForProject->increaseDocumentAttribute( - 'transactions', - $transactionId, - 'operations', - 1 - ); - }); - - // Return successful response without actually upserting document - $mockDocument = new Document([ - '$id' => $documentId, - '$collectionId' => $collectionId, - '$databaseId' => $databaseId, - ...$data - ]); - $response - ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) - ->dynamic($mockDocument, $this->getResponseModel()); - return; - } - $allowedPermissions = [ Database::PERMISSION_READ, Database::PERMISSION_UPDATE, @@ -300,6 +246,60 @@ class Upsert extends Action ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); + } + + // Enforce max operations per transaction + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; + $existing = $transaction->getAttribute('operations', 0); + if (($existing + 1) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch + ); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'upsert', + 'data' => $data, + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually upserting document + $mockDocument = new Document([ + '$id' => $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + ...$data + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($mockDocument, $this->getResponseModel()); + return; + } + $upserted = []; try { $dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {