Merge pull request #10023 from appwrite/feat-txn

Feat txn
This commit is contained in:
Jake Barnby 2025-10-09 16:47:45 +13:00 committed by GitHub
commit 5727b0fb45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
255 changed files with 31292 additions and 1827 deletions

View file

@ -2521,4 +2521,128 @@ return [
],
],
],
'transactions' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('transactions'),
'name' => 'Transactions',
'attributes' => [
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'size' => 16, // pending | committing | committed | failed
'signed' => true,
'required' => false,
'default' => 'pending',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('operations'),
'type' => Database::VAR_INTEGER,
'size' => 0,
'signed' => false,
'required' => true,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
],
],
'transactionLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('transactionLogs'),
'name' => 'Transaction Logs',
'attributes' => [
[
'$id' => ID::custom('transactionInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('databaseInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('collectionInternalId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('documentId'),
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('action'),
'type' => Database::VAR_STRING,
'size' => 32, // create | update | upsert | increment | decrement | delete | bulkCreate | bulkUpdate | bulkUpsert | bulkDelete
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
'size' => 5_000_000, // Allow large payloads for bulk operations
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_transaction'),
'type' => Database::INDEX_KEY,
'attributes' => ['transactionInternalId'],
'lengths' => [],
'orders' => [],
],
],
],
];

View file

@ -981,6 +981,48 @@ return [
'code' => 409,
],
/** Transactions */
Exception::TRANSACTION_NOT_FOUND => [
'name' => Exception::TRANSACTION_NOT_FOUND,
'description' => 'Transaction with the requested ID could not be found.',
'code' => 404,
],
Exception::TRANSACTION_ALREADY_EXISTS => [
'name' => Exception::TRANSACTION_ALREADY_EXISTS,
'description' => 'Transaction with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TRANSACTION_INVALID => [
'name' => Exception::TRANSACTION_INVALID,
'description' => 'The transaction is invalid. Please check the transaction state and try again.',
'code' => 400,
],
Exception::TRANSACTION_FAILED => [
'name' => Exception::TRANSACTION_FAILED,
'description' => 'The transaction has errored. Please check the transaction data and try again.',
'code' => 400,
],
Exception::TRANSACTION_EXPIRED => [
'name' => Exception::TRANSACTION_EXPIRED,
'description' => 'The transaction has expired. Please create a new transaction and try again.',
'code' => 410,
],
Exception::TRANSACTION_CONFLICT => [
'name' => Exception::TRANSACTION_CONFLICT,
'description' => 'The transaction has a conflict. Please resolve the conflict and try again.',
'code' => 409,
],
Exception::TRANSACTION_LIMIT_EXCEEDED => [
'name' => Exception::TRANSACTION_LIMIT_EXCEEDED,
'description' => 'The maximum number of operations per transaction has been exceeded.',
'code' => 400,
],
Exception::TRANSACTION_NOT_READY => [
'name' => Exception::TRANSACTION_NOT_READY,
'description' => 'The transaction is not ready yet. Please try again later.',
'code' => 400,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
'name' => Exception::PROJECT_NOT_FOUND,

View file

@ -24,7 +24,7 @@ return [
'gitUrl' => 'git@github.com:appwrite/sdk-for-web.git',
'gitRepoName' => 'sdk-for-web',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'gitBranch' => 'feat-txn',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/web/CHANGELOG.md'),
'demos' => [
[
@ -275,7 +275,7 @@ return [
'gitUrl' => 'git@github.com:appwrite/sdk-for-node.git',
'gitRepoName' => 'sdk-for-node',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'gitBranch' => 'feat-txn',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/nodejs/CHANGELOG.md'),
],
[

View file

@ -28,7 +28,7 @@ $member = [
'subscribers.write',
'subscribers.read',
'assistant.read',
'rules.read'
'rules.read',
];
$admins = [

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1253,7 +1253,7 @@ App::error()
}
/**
* If its not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
@ -1355,6 +1355,7 @@ App::error()
case 409: // Error allowed publicly
case 412: // Error allowed publicly
case 416: // Error allowed publicly
case 422: // Error allowed publicly
case 429: // Error allowed publicly
case 451: // Error allowed publicly
case 501: // Error allowed publicly

View file

@ -31,6 +31,7 @@ const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
const APP_LIMIT_DATABASE_BATCH = 100; // Default maximum batch size for database operations
const APP_LIMIT_DATABASE_TRANSACTION = 100; // Default maximum operations per transaction
const APP_KEY_ACCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
@ -55,6 +56,10 @@ const APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER = 300 * 1000; // 5 minutes
const APP_DATABASE_TIMEOUT_MILLISECONDS_TASK = 300 * 1000; // 5 minutes
const APP_DATABASE_QUERY_MAX_VALUES = 500;
const APP_DATABASE_ENCRYPT_SIZE_MIN = 150;
const APP_DATABASE_TXN_TTL_MIN = 60; // 1 minute
const APP_DATABASE_TXN_TTL_MAX = 3600; // 1 hour
const APP_DATABASE_TXN_TTL_DEFAULT = 300; // 5 minutes
const APP_DATABASE_TXN_MAX_OPERATIONS = 100; // Maximum operations per transaction
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_SITES = '/storage/sites';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
@ -105,8 +110,11 @@ const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
const DELETE_TYPE_EXPIRED_TRANSACTIONS = 'expired_transactions';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_SITES = 'sites';
const DELETE_TYPE_FUNCTIONS = 'functions';

View file

@ -4,6 +4,7 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@ -1041,3 +1042,7 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
App::setResource('transactionState', function (Database $dbForProject) {
return new TransactionState($dbForProject);
}, ['dbForProject']);

1
composer.lock generated
View file

@ -5127,6 +5127,7 @@
"issues": "https://github.com/doctrine/annotations/issues",
"source": "https://github.com/doctrine/annotations/tree/2.0.2"
},
"abandoned": true,
"time": "2024-09-05T10:17:24+00:00"
},
{

View file

@ -17,7 +17,8 @@ const result = await databases.createDocument({
"age": 30,
"isAdmin": false
},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,24 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.createOperations({
transactionId: '<TRANSACTION_ID>',
operations: [
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"collectionId": "<COLLECTION_ID>",
"documentId": "<DOCUMENT_ID>",
"data": {
"name": "Walter O'Brien"
}
}
] // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.createTransaction({
ttl: 60 // optional
});
console.log(result);

View file

@ -12,7 +12,8 @@ const result = await databases.decrementDocumentAttribute({
documentId: '<DOCUMENT_ID>',
attribute: '',
value: null, // optional
min: null // optional
min: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -9,7 +9,8 @@ const databases = new Databases(client);
const result = await databases.deleteDocument({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>'
documentId: '<DOCUMENT_ID>',
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.deleteTransaction({
transactionId: '<TRANSACTION_ID>'
});
console.log(result);

View file

@ -10,7 +10,8 @@ const result = await databases.getDocument({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.getTransaction({
transactionId: '<TRANSACTION_ID>'
});
console.log(result);

View file

@ -12,7 +12,8 @@ const result = await databases.incrementDocumentAttribute({
documentId: '<DOCUMENT_ID>',
attribute: '',
value: null, // optional
max: null // optional
max: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -9,7 +9,8 @@ const databases = new Databases(client);
const result = await databases.listDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.listTransactions({
queries: [] // optional
});
console.log(result);

View file

@ -11,7 +11,8 @@ const result = await databases.updateDocument({
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,15 @@
import { Client, Databases } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const databases = new Databases(client);
const result = await databases.updateTransaction({
transactionId: '<TRANSACTION_ID>',
commit: false, // optional
rollback: false // optional
});
console.log(result);

View file

@ -11,7 +11,8 @@ const result = await databases.upsertDocument({
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
data: {},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,24 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.createOperations({
transactionId: '<TRANSACTION_ID>',
operations: [
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"tableId": "<TABLE_ID>",
"rowId": "<ROW_ID>",
"data": {
"name": "Walter O'Brien"
}
}
] // optional
});
console.log(result);

View file

@ -17,7 +17,8 @@ const result = await tablesDB.createRow({
"age": 30,
"isAdmin": false
},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.createTransaction({
ttl: 60 // optional
});
console.log(result);

View file

@ -12,7 +12,8 @@ const result = await tablesDB.decrementRowColumn({
rowId: '<ROW_ID>',
column: '',
value: null, // optional
min: null // optional
min: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -9,7 +9,8 @@ const tablesDB = new TablesDB(client);
const result = await tablesDB.deleteRow({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>'
rowId: '<ROW_ID>',
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.deleteTransaction({
transactionId: '<TRANSACTION_ID>'
});
console.log(result);

View file

@ -10,7 +10,8 @@ const result = await tablesDB.getRow({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.getTransaction({
transactionId: '<TRANSACTION_ID>'
});
console.log(result);

View file

@ -12,7 +12,8 @@ const result = await tablesDB.incrementRowColumn({
rowId: '<ROW_ID>',
column: '',
value: null, // optional
max: null // optional
max: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -9,7 +9,8 @@ const tablesDB = new TablesDB(client);
const result = await tablesDB.listRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,13 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.listTransactions({
queries: [] // optional
});
console.log(result);

View file

@ -11,7 +11,8 @@ const result = await tablesDB.updateRow({
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -0,0 +1,15 @@
import { Client, TablesDB } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const tablesDB = new TablesDB(client);
const result = await tablesDB.updateTransaction({
transactionId: '<TRANSACTION_ID>',
commit: false, // optional
rollback: false // optional
});
console.log(result);

View file

@ -11,7 +11,8 @@ const result = await tablesDB.upsertRow({
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});
console.log(result);

View file

@ -18,5 +18,6 @@ const result = await databases.createDocument({
"age": 30,
"isAdmin": false
},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const databases = new sdk.Databases(client);
const result = await databases.createDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documents: []
documents: [],
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,23 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.createOperations({
transactionId: '<TRANSACTION_ID>',
operations: [
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"collectionId": "<COLLECTION_ID>",
"documentId": "<DOCUMENT_ID>",
"data": {
"name": "Walter O'Brien"
}
}
] // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.createTransaction({
ttl: 60 // optional
});

View file

@ -13,5 +13,6 @@ const result = await databases.decrementDocumentAttribute({
documentId: '<DOCUMENT_ID>',
attribute: '',
value: null, // optional
min: null // optional
min: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const databases = new sdk.Databases(client);
const result = await databases.deleteDocument({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>'
documentId: '<DOCUMENT_ID>',
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const databases = new sdk.Databases(client);
const result = await databases.deleteDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.deleteTransaction({
transactionId: '<TRANSACTION_ID>'
});

View file

@ -11,5 +11,6 @@ const result = await databases.getDocument({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.getTransaction({
transactionId: '<TRANSACTION_ID>'
});

View file

@ -13,5 +13,6 @@ const result = await databases.incrementDocumentAttribute({
documentId: '<DOCUMENT_ID>',
attribute: '',
value: null, // optional
max: null // optional
max: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const databases = new sdk.Databases(client);
const result = await databases.listDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.listTransactions({
queries: [] // optional
});

View file

@ -12,5 +12,6 @@ const result = await databases.updateDocument({
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -11,5 +11,6 @@ const result = await databases.updateDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
data: {}, // optional
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,14 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const databases = new sdk.Databases(client);
const result = await databases.updateTransaction({
transactionId: '<TRANSACTION_ID>',
commit: false, // optional
rollback: false // optional
});

View file

@ -12,5 +12,6 @@ const result = await databases.upsertDocument({
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
data: {},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const databases = new sdk.Databases(client);
const result = await databases.upsertDocuments({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documents: []
documents: [],
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -16,7 +16,7 @@ const result = await messaging.createPush({
targets: [], // optional
data: {}, // optional
action: '<ACTION>', // optional
image: '[ID1:ID2]', // optional
image: '<ID1:ID2>', // optional
icon: '<ICON>', // optional
sound: '<SOUND>', // optional
color: '<COLOR>', // optional

View file

@ -16,7 +16,7 @@ const result = await messaging.updatePush({
body: '<BODY>', // optional
data: {}, // optional
action: '<ACTION>', // optional
image: '[ID1:ID2]', // optional
image: '<ID1:ID2>', // optional
icon: '<ICON>', // optional
sound: '<SOUND>', // optional
color: '<COLOR>', // optional

View file

@ -0,0 +1,23 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.createOperations({
transactionId: '<TRANSACTION_ID>',
operations: [
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"tableId": "<TABLE_ID>",
"rowId": "<ROW_ID>",
"data": {
"name": "Walter O'Brien"
}
}
] // optional
});

View file

@ -18,5 +18,6 @@ const result = await tablesDB.createRow({
"age": 30,
"isAdmin": false
},
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.createRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rows: []
rows: [],
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.createTransaction({
ttl: 60 // optional
});

View file

@ -13,5 +13,6 @@ const result = await tablesDB.decrementRowColumn({
rowId: '<ROW_ID>',
column: '',
value: null, // optional
min: null // optional
min: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.deleteRow({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>'
rowId: '<ROW_ID>',
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.deleteRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.deleteTransaction({
transactionId: '<TRANSACTION_ID>'
});

View file

@ -11,5 +11,6 @@ const result = await tablesDB.getRow({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.getTransaction({
transactionId: '<TRANSACTION_ID>'
});

View file

@ -13,5 +13,6 @@ const result = await tablesDB.incrementRowColumn({
rowId: '<ROW_ID>',
column: '',
value: null, // optional
max: null // optional
max: null, // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.listRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,12 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.listTransactions({
queries: [] // optional
});

View file

@ -12,5 +12,6 @@ const result = await tablesDB.updateRow({
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -11,5 +11,6 @@ const result = await tablesDB.updateRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
data: {}, // optional
queries: [] // optional
queries: [], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1,14 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setKey('<YOUR_API_KEY>'); // Your secret API key
const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.updateTransaction({
transactionId: '<TRANSACTION_ID>',
commit: false, // optional
rollback: false // optional
});

View file

@ -12,5 +12,6 @@ const result = await tablesDB.upsertRow({
tableId: '<TABLE_ID>',
rowId: '<ROW_ID>',
data: {}, // optional
permissions: ["read("any")"] // optional
permissions: ["read("any")"], // optional
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client);
const result = await tablesDB.upsertRows({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rows: []
rows: [],
transactionId: '<TRANSACTION_ID>' // optional
});

View file

@ -0,0 +1 @@
Create multiple operations in a single transaction.

View file

@ -0,0 +1 @@
Create a new transaction.

View file

@ -0,0 +1 @@
Delete a transaction by its unique ID.

View file

@ -1 +1 @@
Get index by ID.
Get an index by its unique ID.

View file

@ -0,0 +1 @@
Get a transaction by its unique ID.

View file

@ -0,0 +1 @@
List transactions across all databases.

View file

@ -0,0 +1 @@
Update a transaction, to either commit or roll back its operations.

View file

@ -0,0 +1 @@
Create multiple operations in a single transaction.

View file

@ -0,0 +1 @@
Create a new transaction.

View file

@ -0,0 +1 @@
Delete a transaction by its unique ID.

View file

@ -0,0 +1 @@
Get a transaction by its unique ID.

View file

@ -0,0 +1 @@
List transactions across all databases.

View file

@ -0,0 +1 @@
Update a transaction, to either commit or roll back its operations.

View file

@ -0,0 +1,745 @@
<?php
namespace Appwrite\Databases;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Exception\Timeout;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
/**
* Service for managing transaction state and providing transaction-aware document operations
*
* This class provides methods to:
* - Query documents with transaction awareness (getDocument, listDocuments, countDocuments)
* - Apply bulk operations to transaction state for cross-operation visibility
* - Replay transaction operations to build current state
*/
class TransactionState
{
private Database $dbForProject;
public function __construct(Database $dbForProject)
{
$this->dbForProject = $dbForProject;
}
/**
* Get a document with transaction-aware logic
*
* @param string $collectionId Collection ID
* @param string $documentId Document ID
* @param string|null $transactionId Optional transaction ID
* @param array $queries Optional query filters
* @return Document
* @throws Exception
* @throws Exception\Query
* @throws Timeout
*/
public function getDocument(
string $collectionId,
string $documentId,
?string $transactionId = null,
array $queries = []
): Document {
if ($transactionId === null) {
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
}
$state = $this->getTransactionState($transactionId);
if (isset($state[$collectionId][$documentId])) {
$docState = $state[$collectionId][$documentId];
if (!$docState['exists']) {
return new Document();
}
if ($docState['action'] === 'create') {
return $this->applyProjection($docState['document'], $queries);
}
if ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
// Merge with committed version
$committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries);
if (!$committedDoc->isEmpty()) {
foreach ($docState['document']->getAttributes() as $key => $value) {
if ($key !== '$id') {
$committedDoc->setAttribute($key, $value);
}
}
// Reapply projection in case transaction added new fields
return $this->applyProjection($committedDoc, $queries);
} elseif ($docState['action'] === 'upsert') {
return $this->applyProjection($docState['document'], $queries);
}
}
}
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
}
/**
* List documents with transaction-aware logic
*
* @param string $collectionId Collection ID
* @param string|null $transactionId Optional transaction ID
* @param array $queries Optional query filters
* @return array Array of Document objects
* @throws Exception
* @throws Exception\Query
* @throws Timeout
*/
public function listDocuments(
string $collectionId,
?string $transactionId = null,
array $queries = []
): array {
// If no transaction, use normal database retrieval
if ($transactionId === null) {
return $this->dbForProject->find($collectionId, $queries);
}
$state = $this->getTransactionState($transactionId);
$committedDocs = $this->dbForProject->find($collectionId, $queries);
$documentMap = [];
// Build map of committed documents
foreach ($committedDocs as $doc) {
$documentMap[$doc->getId()] = $doc;
}
// Apply transaction state changes
if (isset($state[$collectionId])) {
foreach ($state[$collectionId] as $docId => $docState) {
if (!$docState['exists']) {
// Document was deleted, remove from results
unset($documentMap[$docId]);
} elseif ($docState['action'] === 'create') {
// Document was created, add to results with projection
$documentMap[$docId] = $this->applyProjection($docState['document'], $queries);
} elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
if (isset($documentMap[$docId])) {
// Update existing document
foreach ($docState['document']->getAttributes() as $key => $value) {
if ($key !== '$id') {
$documentMap[$docId]->setAttribute($key, $value);
}
}
// Reapply projection in case transaction added new fields
$documentMap[$docId] = $this->applyProjection($documentMap[$docId], $queries);
} elseif ($docState['action'] === 'upsert') {
// Upsert created a new document, apply projection
$documentMap[$docId] = $this->applyProjection($docState['document'], $queries);
}
}
}
}
return array_values($documentMap);
}
/**
* Count documents with transaction-aware logic
*
* @param string $collectionId Collection ID
* @param string|null $transactionId Optional transaction ID
* @param array $queries Optional query filters
* @return int Document count
* @throws Exception
* @throws Exception\Query
* @throws Timeout
*/
public function countDocuments(
string $collectionId,
?string $transactionId = null,
array $queries = []
): int {
if ($transactionId === null) {
return $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
}
$state = $this->getTransactionState($transactionId);
$baseCount = $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
if (!isset($state[$collectionId])) {
return $baseCount;
}
$committedDocs = $this->dbForProject->find($collectionId, $queries);
$committedDocIds = [];
foreach ($committedDocs as $doc) {
$committedDocIds[$doc->getId()] = true;
}
$adjustedCount = $baseCount;
$filters = $this->extractFilters($queries);
foreach ($state[$collectionId] as $docId => $docState) {
if (!$docState['exists']) {
if (isset($committedDocIds[$docId])) {
$adjustedCount--;
}
} elseif ($docState['action'] === 'create') {
if ($this->documentMatchesFilters($docState['document'], $filters)) {
$adjustedCount++;
}
} elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
$wasInResults = isset($committedDocIds[$docId]);
$nowMatches = $this->documentMatchesFilters($docState['document'], $filters);
if (!$wasInResults && $nowMatches && $docState['action'] === 'upsert') {
$adjustedCount++;
} elseif ($wasInResults && !$nowMatches) {
$adjustedCount--;
} elseif (!$wasInResults && $nowMatches) {
// Update shouldn't add a new doc, but upsert might have
if ($docState['action'] === 'upsert') {
$adjustedCount++;
}
}
}
}
return max(0, $adjustedCount);
}
/**
* Check if a document exists with transaction-aware logic
*
* @param string $collectionId Collection ID
* @param string $documentId Document ID
* @param string|null $transactionId Optional transaction ID
* @return bool True if document exists
*/
public function documentExists(
string $collectionId,
string $documentId,
?string $transactionId = null
): bool {
$doc = $this->getDocument($collectionId, $documentId, $transactionId);
return !$doc->isEmpty();
}
/**
* Apply bulk update to documents in transaction state that match queries
*
* This allows bulk operations within a transaction to see each other's changes.
*
* @param string $collectionId Collection ID
* @param Document $updateData Document with update values
* @param array $queries Query filters to match documents
* @param array &$state Transaction state (passed by reference)
* @return void
*/
public function applyBulkUpdateToState(
string $collectionId,
Document $updateData,
array $queries,
array &$state
): void {
if (!isset($state[$collectionId])) {
return;
}
$filters = $this->extractFilters($queries);
foreach ($state[$collectionId] as $docId => $doc) {
if ($this->documentMatchesFilters($doc, $filters)) {
foreach ($updateData->getArrayCopy() as $key => $value) {
if ($key !== '$id') {
$doc->setAttribute($key, $value);
}
}
}
}
}
/**
* Apply bulk delete to documents in transaction state that match queries
*
* This allows bulk operations within a transaction to see each other's changes.
*
* @param string $collectionId Collection ID
* @param array $queries Query filters to match documents
* @param array &$state Transaction state (passed by reference)
* @return void
*/
public function applyBulkDeleteToState(
string $collectionId,
array $queries,
array &$state
): void {
if (!isset($state[$collectionId])) {
return;
}
$filters = $this->extractFilters($queries);
foreach ($state[$collectionId] as $docId => $doc) {
if ($this->documentMatchesFilters($doc, $filters)) {
unset($state[$collectionId][$docId]);
}
}
}
/**
* Apply bulk upsert to documents in transaction state
*
* This merges partial upsert data with full documents from transaction state,
* preventing validation errors when upserting documents created in the same transaction.
*
* @param string $collectionId Collection ID
* @param array $documents Array of Document objects to upsert (can be partial)
* @param array &$state Transaction state (passed by reference)
* @return array Merged documents ready for database upsert
*/
public function applyBulkUpsertToState(
string $collectionId,
array $documents,
array &$state
): array {
$mergedDocuments = [];
foreach ($documents as $doc) {
if (!($doc instanceof Document)) {
continue;
}
$docId = $doc->getId();
if (!$docId) {
continue;
}
if (isset($state[$collectionId][$docId])) {
foreach ($doc->getArrayCopy() as $key => $value) {
if ($key !== '$id') {
$state[$collectionId][$docId]->setAttribute($key, $value);
}
}
$mergedDocuments[] = $state[$collectionId][$docId];
} else {
$mergedDocuments[] = $doc;
}
}
return $mergedDocuments;
}
/**
* Get the current state of a transaction by replaying its operations
*
* @param string $transactionId Transaction ID
* @return array State array with structure: [collectionId => [docId => ['action' => ..., 'document' => ..., 'exists' => ...]]]
* @throws Exception
* @throws Exception\Query
* @throws Timeout
*/
private function getTransactionState(string $transactionId): array
{
$transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') {
return [];
}
$operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX)
]));
$state = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
$collectionInternalId = $operation['collectionInternalId'];
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
$documentId = $operation['documentId'];
$action = $operation['action'];
$data = $operation['data'];
if ($data instanceof Document) {
$data = $data->getArrayCopy();
}
switch ($action) {
case 'create':
$docId = $documentId ?? ($data['$id'] ?? null);
if ($docId) {
if (!isset($data['$id'])) {
$data['$id'] = $docId;
}
$state[$collectionId][$docId] = [
'action' => 'create',
'document' => new Document($data),
'exists' => true
];
}
break;
case 'update':
if (isset($state[$collectionId][$documentId])) {
$existingDocument = $state[$collectionId][$documentId]['document'];
foreach ($data as $key => $value) {
if ($key !== '$id') {
$existingDocument->setAttribute($key, $value);
}
}
// Only set action to 'update' if it's not already 'create' or 'upsert'
$currentAction = $state[$collectionId][$documentId]['action'];
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
$state[$collectionId][$documentId]['action'] = 'update';
}
} else {
$state[$collectionId][$documentId] = [
'action' => 'update',
'document' => new Document($data),
'exists' => true
];
}
break;
case 'upsert':
$docId = $documentId ?? ($data['$id'] ?? null);
if (!$docId) {
break;
}
$state[$collectionId][$docId] = [
'action' => 'upsert',
'document' => new Document($data),
'exists' => true
];
break;
case 'delete':
$state[$collectionId][$documentId] = [
'action' => 'delete',
'exists' => false
];
break;
case 'increment':
case 'decrement':
$attribute = $data['attribute'] ?? null;
$value = $data['value'] ?? 1;
if ($attribute) {
if (isset($state[$collectionId][$documentId])) {
$existingDocument = $state[$collectionId][$documentId]['document'];
$currentValue = $existingDocument->getAttribute($attribute, 0);
$newValue = $action === 'increment' ? $currentValue + $value : $currentValue - $value;
$existingDocument->setAttribute($attribute, $newValue);
$currentAction = $state[$collectionId][$documentId]['action'];
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
$state[$collectionId][$documentId]['action'] = 'update';
}
} else {
$newValue = $action === 'increment' ? $value : -$value;
$state[$collectionId][$documentId] = [
'action' => 'update',
'document' => new Document([$attribute => $newValue]),
'exists' => true
];
}
}
break;
case 'bulkCreate':
if (\is_array($data)) {
foreach ($data as $doc) {
if ($doc instanceof Document) {
$doc = $doc->getArrayCopy();
}
$state[$collectionId][$doc['$id']] = [
'action' => 'create',
'document' => new Document($doc),
'exists' => true
];
}
}
break;
case 'bulkUpdate':
if (isset($data['queries']) && isset($data['data'])) {
$queries = Query::parseQueries($data['queries'] ?? []);
$updateData = $data['data'];
foreach ($state[$collectionId] ?? [] as $docId => $entry) {
if (!$entry['exists']) {
continue;
}
$document = $entry['document'];
$filters = $this->extractFilters($queries);
if ($this->documentMatchesFilters($document, $filters)) {
foreach ($updateData as $key => $value) {
if ($key !== '$id') {
$document->setAttribute($key, $value);
}
}
$currentAction = $state[$collectionId][$docId]['action'];
if ($currentAction !== 'create' && $currentAction !== 'upsert') {
$state[$collectionId][$docId]['action'] = 'update';
}
}
}
}
break;
case 'bulkUpsert':
if (\is_array($data)) {
foreach ($data as $doc) {
if ($doc instanceof Document) {
$doc = $doc->getArrayCopy();
}
$docId = $doc['$id'] ?? null;
if (!$docId) {
continue;
}
if (isset($state[$collectionId][$docId])) {
$existingDocument = $state[$collectionId][$docId]['document'];
foreach ($doc as $key => $value) {
$existingDocument->setAttribute($key, $value);
}
} else {
$state[$collectionId][$docId] = [
'action' => 'upsert',
'document' => new Document($doc),
'exists' => true
];
}
}
}
break;
case 'bulkDelete':
if (isset($data['queries'])) {
$queries = Query::parseQueries($data['queries'] ?? []);
$filters = $this->extractFilters($queries);
foreach ($state[$collectionId] ?? [] as $docId => $entry) {
if (!$entry['exists']) {
continue;
}
$document = $entry['document'];
if ($this->documentMatchesFilters($document, $filters)) {
$state[$collectionId][$docId] = [
'action' => 'delete',
'exists' => false
];
}
}
}
break;
}
}
return $state;
}
/**
* Apply projection (select) semantics from queries to a document
*
* @param Document $doc Document to apply projection to
* @param array $queries Query array that may contain select queries
* @return Document Projected document
*/
private function applyProjection(Document $doc, array $queries): Document
{
if (empty($queries)) {
return $doc;
}
$selections = [];
foreach ($queries as $query) {
if ($query->getMethod() === Query::TYPE_SELECT) {
$values = $query->getValues();
foreach ($values as $value) {
// Skip relationship selections (containing '.')
if (!\str_contains($value, '.')) {
$selections[] = $value;
}
}
}
}
if (empty($selections) || \in_array('*', $selections)) {
return $doc;
}
// Create a new document with only selected attributes
$projected = new Document();
// Always preserve internal attributes
$projected->setAttribute('$id', $doc->getId());
$projected->setAttribute('$collection', $doc->getCollection());
$projected->setAttribute('$createdAt', $doc->getCreatedAt());
$projected->setAttribute('$updatedAt', $doc->getUpdatedAt());
if ($doc->offsetExists('$permissions')) {
$projected->setAttribute('$permissions', $doc->getPermissions());
}
// Add selected attributes
foreach ($selections as $attribute) {
if ($doc->offsetExists($attribute)) {
$projected->setAttribute($attribute, $doc->getAttribute($attribute));
}
}
return $projected;
}
/**
* Extract only filter queries from a query array
*
* @param array $queries Query array
* @return array Filtered queries
*/
private function extractFilters(array $queries): array
{
$filters = [];
foreach ($queries as $query) {
$method = $query->getMethod();
if (!\in_array($method, [
Query::TYPE_LIMIT,
Query::TYPE_OFFSET,
Query::TYPE_CURSOR_AFTER,
Query::TYPE_CURSOR_BEFORE,
Query::TYPE_SELECT,
Query::TYPE_ORDER_ASC,
Query::TYPE_ORDER_DESC
])) {
$filters[] = $query;
}
}
return $filters;
}
/**
* Check if a document matches filter queries
*
* @param Document $doc Document to check
* @param array $filters Pre-filtered Query filters (use extractFilters first)
* @return bool True if document matches all filters
*/
private function documentMatchesFilters(Document $doc, array $filters): bool
{
if (empty($filters)) {
return true;
}
foreach ($filters as $filter) {
$attribute = $filter->getAttribute();
$values = $filter->getValues();
$docValue = $doc->getAttribute($attribute);
switch ($filter->getMethod()) {
case Query::TYPE_EQUAL:
if (!\in_array($docValue, $values)) {
return false;
}
break;
case Query::TYPE_NOT_EQUAL:
if (\in_array($docValue, $values)) {
return false;
}
break;
case Query::TYPE_CONTAINS:
$matches = false;
foreach ($values as $value) {
if (\is_array($docValue) && \in_array($value, $docValue)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
break;
case Query::TYPE_STARTS_WITH:
$matches = false;
foreach ($values as $value) {
if (\is_string($docValue) && \str_starts_with($docValue, $value)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
break;
case Query::TYPE_ENDS_WITH:
$matches = false;
foreach ($values as $value) {
if (\is_string($docValue) && \str_ends_with($docValue, $value)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
break;
case Query::TYPE_GREATER:
if (!($docValue > $values[0])) {
return false;
}
break;
case Query::TYPE_GREATER_EQUAL:
if (!($docValue >= $values[0])) {
return false;
}
break;
case Query::TYPE_LESSER:
if (!($docValue < $values[0])) {
return false;
}
break;
case Query::TYPE_LESSER_EQUAL:
if (!($docValue <= $values[0])) {
return false;
}
break;
case Query::TYPE_IS_NULL:
if (!\is_null($docValue)) {
return false;
}
break;
case Query::TYPE_IS_NOT_NULL:
if (\is_null($docValue)) {
return false;
}
break;
case Query::TYPE_BETWEEN:
if (!($docValue >= $values[0] && $docValue <= $values[1])) {
return false;
}
break;
}
}
return true;
}
}

Some files were not shown because too many files have changed in this diff Show more