From b556846be3441e1daa3bba3a4c0cf438a31924d7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:01:39 -0400 Subject: [PATCH 001/145] Add txn roles --- app/config/roles.php | 130 ++++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index bccc2837f5..bbe970f05d 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -3,105 +3,107 @@ use Appwrite\Auth\Auth; $member = [ - 'global', - 'public', - 'home', - 'console', - 'graphql', - 'sessions.write', 'account', - 'teams.read', - 'teams.write', + 'assistant.read', + 'avatars.read', + 'console', 'documents.read', 'documents.write', - 'files.read', - 'files.write', - 'projects.read', - 'locale.read', - 'avatars.read', 'execution.read', 'execution.write', + 'files.read', + 'files.write', + 'global', + 'graphql', + 'home', + 'locale.read', + 'projects.read', + 'public', + 'rules.read', + 'sessions.write', + 'subscribers.read', + 'subscribers.write', 'targets.read', 'targets.write', - 'subscribers.write', - 'subscribers.read', - 'assistant.read', - 'rules.read' + 'teams.read', + 'teams.write', ]; $admins = [ - 'global', - 'graphql', - 'sessions.write', - 'teams.read', - 'teams.write', - 'documents.read', - 'documents.write', - 'files.read', - 'files.write', + 'avatars.read', 'buckets.read', 'buckets.write', - 'users.read', - 'users.write', - 'databases.read', - 'databases.write', 'collections.read', 'collections.write', + 'databases.read', + 'databases.write', + 'documents.read', + 'documents.write', + 'execution.read', + 'execution.write', + 'files.read', + 'files.write', + 'functions.read', + 'functions.write', + 'global', + 'graphql', + 'health.read', + 'keys.read', + 'keys.write', + 'locale.read', + 'log.read', + 'log.write', + 'messages.read', + 'messages.write', + 'migrations.read', + 'migrations.write', 'platforms.read', 'platforms.write', 'projects.write', - 'keys.read', - 'keys.write', - 'webhooks.read', - 'webhooks.write', - 'locale.read', - 'avatars.read', - 'health.read', - 'functions.read', - 'functions.write', - 'sites.read', - 'sites.write', - 'log.read', - 'log.write', - 'execution.read', - 'execution.write', + 'providers.read', + 'providers.write', 'rules.read', 'rules.write', - 'migrations.read', - 'migrations.write', - 'vcs.read', - 'vcs.write', + 'sessions.write', + 'sites.read', + 'sites.write', + 'subscribers.read', + 'subscribers.write', 'targets.read', 'targets.write', - 'providers.write', - 'providers.read', - 'messages.write', - 'messages.read', - 'topics.write', - 'topics.read', - 'subscribers.write', - 'subscribers.read', + 'teams.read', + 'teams.write', 'tokens.read', 'tokens.write', + 'topics.read', + 'topics.write', + 'transactions.read', + 'transactions.write', + 'users.read', + 'users.write', + 'vcs.read', + 'vcs.write', + 'webhooks.read', + 'webhooks.write', ]; return [ Auth::USER_ROLE_GUESTS => [ 'label' => 'Guests', 'scopes' => [ - 'global', - 'public', - 'home', + 'avatars.read', 'console', - 'graphql', - 'sessions.write', 'documents.read', 'documents.write', + 'execution.write', 'files.read', 'files.write', + 'global', + 'graphql', + 'home', 'locale.read', - 'avatars.read', - 'execution.write', + 'public', + 'sessions.write', ], ], Auth::USER_ROLE_USERS => [ From fc4b82d017a85730b37e4c17266598c96725c38a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:01:45 -0400 Subject: [PATCH 002/145] Add txn scopes --- app/config/scopes.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/config/scopes.php b/app/config/scopes.php index 7dea7b1cd5..69cc94a009 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -40,6 +40,12 @@ return [ // List of publicly visible scopes 'indexes.write' => [ 'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes', ], + 'transactions.read' => [ + 'description' => 'Access to read your project\'s database transactions', + ], + 'transactions.write' => [ + 'description' => 'Access to create, update, and delete your project\'s database transactions', + ], 'documents.read' => [ 'description' => 'Access to read your project\'s database documents', ], From 11ce3941811f97521b6000390b3d70ef648e797c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:01:56 -0400 Subject: [PATCH 003/145] Add txn errors --- app/config/errors.php | 37 +++++++++++++++++++++++++++++++ src/Appwrite/Extend/Exception.php | 10 +++++++++ 2 files changed, 47 insertions(+) diff --git a/app/config/errors.php b/app/config/errors.php index 8365e8c705..7a48e7f46f 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -810,6 +810,43 @@ 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 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 transactions has been reached.', + '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, diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 3af6d9962c..2e9911683c 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -229,6 +229,16 @@ class Exception extends \Exception public const INDEX_INVALID = 'index_invalid'; public const INDEX_DEPENDENCY = 'index_dependency'; + /** Transactions */ + public const TRANSACTION_NOT_FOUND = 'transaction_not_found'; + public const TRANSACTION_ALREADY_EXISTS = 'transaction_already_exists'; + public const TRANSACTION_INVALID = 'transaction_invalid'; + public const TRANSACTION_EXPIRED = 'transaction_expired'; + public const TRANSACTION_CONFLICT = 'transaction_conflict'; + public const TRANSACTION_LIMIT_EXCEEDED = 'transaction_limit_exceeded'; + public const TRANSACTION_NOT_READY = 'transaction_not_ready'; + + /** Projects */ public const PROJECT_NOT_FOUND = 'project_not_found'; public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled'; From c11fd38f0fa3500c46b72c12596add82ea673547 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:02:17 -0400 Subject: [PATCH 004/145] Add txn models --- src/Appwrite/SDK/Response.php | 3 +- .../Utopia/Database/Validator/Operation.php | 76 +++++ src/Appwrite/Utopia/Response.php | 285 +++++++++--------- .../Utopia/Response/Model/Transaction.php | 54 ++++ 4 files changed, 271 insertions(+), 147 deletions(-) create mode 100644 src/Appwrite/Utopia/Database/Validator/Operation.php create mode 100644 src/Appwrite/Utopia/Response/Model/Transaction.php diff --git a/src/Appwrite/SDK/Response.php b/src/Appwrite/SDK/Response.php index e87813024b..2b034691a8 100644 --- a/src/Appwrite/SDK/Response.php +++ b/src/Appwrite/SDK/Response.php @@ -2,12 +2,11 @@ namespace Appwrite\SDK; -class Response +readonly class Response { /** * @param int $code * @param string|array $model - * @param string $description */ public function __construct( private int $code, diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php new file mode 100644 index 0000000000..3f9a15673a --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -0,0 +1,76 @@ +description; + } + + public function isArray(): bool + { + return true; + } + + /** + * @param mixed $value + */ + public function isValid($value): bool + { + // Must be array‑like + if (!\is_array($value)) { + $this->description = 'Value must be an array'; + return false; + } + + // Mandatory keys + $required = ['databaseId', 'collectionId', 'action', 'payload']; + foreach ($required as $key) { + if (!\array_key_exists($key, $value)) { + $this->description = "Missing required key: {$key}"; + return false; + } + } + + // databaseId / collectionId / action must be non‑empty strings + foreach (['databaseId', 'collectionId', 'action'] as $key) { + if (!\is_string($value[$key]) || \trim($value[$key]) === '') { + $this->description = "Key '{$key}' must be a non‑empty string"; + return false; + } + } + + // Validate action + if (!\in_array($value['action'], $this->actions, true)) { + $this->description = "Key 'action' must be one of: " . \implode(', ', $this->actions); + return false; + } + + // Payload must be array (can be empty) + if (!\is_array($value['payload'])) { + $this->description = "Key 'payload' must be an array"; + return false; + } + + return true; + } + + public function getType(): string + { + return self::TYPE_OBJECT; + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 3d69ac1291..a376dd9cb6 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -144,7 +144,6 @@ class Response extends SwooleResponse public const MODEL_METRIC_LIST = 'metricList'; public const MODEL_METRIC_BREAKDOWN = 'metricBreakdown'; public const MODEL_ERROR_DEV = 'errorDev'; - public const MODEL_BASE_LIST = 'baseList'; public const MODEL_USAGE_DATABASES = 'usageDatabases'; public const MODEL_USAGE_DATABASE = 'usageDatabase'; public const MODEL_USAGE_COLLECTION = 'usageCollection'; @@ -166,6 +165,8 @@ class Response extends SwooleResponse public const MODEL_INDEX_LIST = 'indexList'; public const MODEL_DOCUMENT = 'document'; public const MODEL_DOCUMENT_LIST = 'documentList'; + public const MODEL_TRANSACTION = 'transaction'; + public const MODEL_TRANSACTION_LIST = 'transactionList'; // Database Attributes public const MODEL_ATTRIBUTE = 'attribute'; @@ -337,13 +338,6 @@ class Response extends SwooleResponse // Console public const MODEL_CONSOLE_VARIABLES = 'consoleVariables'; - // Deprecated - public const MODEL_PERMISSIONS = 'permissions'; - public const MODEL_RULE = 'rule'; - public const MODEL_TASK = 'task'; - public const MODEL_DOMAIN = 'domain'; - public const MODEL_DOMAIN_LIST = 'domainList'; - // Tests (keep last) public const MODEL_MOCK = 'mock'; @@ -365,7 +359,7 @@ class Response extends SwooleResponse /** * Response constructor. * - * @param float $time + * @param SwooleHTTPResponse $response */ public function __construct(SwooleHTTPResponse $response) { @@ -376,165 +370,166 @@ class Response extends SwooleResponse ->setModel(new Error()) ->setModel(new ErrorDev()) // Lists - ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) - ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) - ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) - ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) - ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) - ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) - ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) - ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) - ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) - ->setModel(new BaseList('Resource Tokens List', self::MODEL_RESOURCE_TOKEN_LIST, 'tokens', self::MODEL_RESOURCE_TOKEN)) - ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) - ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) - ->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE)) - ->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE)) - ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) - ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) - ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) - ->setModel(new BaseList('Framework Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK)) - ->setModel(new BaseList('Runtime Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_RUNTIME)) - ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) - ->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK)) - ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) - ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) - ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) - ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) - ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Dev Keys List', self::MODEL_DEV_KEY_LIST, 'devKeys', self::MODEL_DEV_KEY, true, false)) ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) - ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) - ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) + ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) + ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) + ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) - ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) + ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) - ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) - ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) - ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) - ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) + ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) + ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) + ->setModel(new BaseList('Dev Keys List', self::MODEL_DEV_KEY_LIST, 'devKeys', self::MODEL_DEV_KEY, true, false)) + ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) + ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) + ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) + ->setModel(new BaseList('Framework Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK)) + ->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK)) + ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) + ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) + ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) + ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) + ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) + ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) - ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) - ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) - ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) + ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) + ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) + ->setModel(new BaseList('Message List', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) + ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) + ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) + ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) + ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) + ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) + ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) + ->setModel(new BaseList('Provider List', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) + ->setModel(new BaseList('Resource Tokens List', self::MODEL_RESOURCE_TOKEN_LIST, 'tokens', self::MODEL_RESOURCE_TOKEN)) + ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) + ->setModel(new BaseList('Runtime Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_RUNTIME)) + ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) + ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) + ->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE)) + ->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE)) + ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) + ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) - ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) - ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) - ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) + ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) + ->setModel(new BaseList('Topic List', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) + ->setModel(new BaseList('Transaction List', self::MODEL_TRANSACTION_LIST, 'transactions', self::MODEL_TRANSACTION)) + ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) + ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) + ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) // Entities - ->setModel(new Database()) - ->setModel(new Collection()) - ->setModel(new Attribute()) - ->setModel(new AttributeList()) - ->setModel(new AttributeString()) - ->setModel(new AttributeInteger()) - ->setModel(new AttributeFloat()) - ->setModel(new AttributeBoolean()) - ->setModel(new AttributeEmail()) - ->setModel(new AttributeEnum()) - ->setModel(new AttributeIP()) - ->setModel(new AttributeURL()) - ->setModel(new AttributeDatetime()) - ->setModel(new AttributeRelationship()) - ->setModel(new Index()) - ->setModel(new ModelDocument()) - ->setModel(new Log()) - ->setModel(new User()) - ->setModel(new AlgoMd5()) - ->setModel(new AlgoSha()) - ->setModel(new AlgoPhpass()) + ->setModel(new Account()) + ->setModel(new AlgoArgon2()) ->setModel(new AlgoBcrypt()) + ->setModel(new AlgoMd5()) + ->setModel(new AlgoPhpass()) ->setModel(new AlgoScrypt()) ->setModel(new AlgoScryptModified()) - ->setModel(new AlgoArgon2()) - ->setModel(new Account()) - ->setModel(new Preferences()) - ->setModel(new Session()) + ->setModel(new AlgoSha()) + ->setModel(new Attribute()) + ->setModel(new AttributeBoolean()) + ->setModel(new AttributeDatetime()) + ->setModel(new AttributeEmail()) + ->setModel(new AttributeEnum()) + ->setModel(new AttributeFloat()) + ->setModel(new AttributeIP()) + ->setModel(new AttributeInteger()) + ->setModel(new AttributeList()) + ->setModel(new AttributeRelationship()) + ->setModel(new AttributeString()) + ->setModel(new AttributeURL()) + ->setModel(new AuthProvider()) + ->setModel(new Branch()) + ->setModel(new Bucket()) + ->setModel(new Collection()) + ->setModel(new ConsoleVariables()) + ->setModel(new Continent()) + ->setModel(new Country()) + ->setModel(new Currency()) + ->setModel(new Database()) + ->setModel(new Deployment()) + ->setModel(new DetectionFramework()) + ->setModel(new DetectionRuntime()) + ->setModel(new DevKey()) + ->setModel(new Execution()) + ->setModel(new File()) + ->setModel(new Framework()) + ->setModel(new FrameworkAdapter()) + ->setModel(new Func()) + ->setModel(new Headers()) + ->setModel(new HealthAntivirus()) + ->setModel(new HealthCertificate()) + ->setModel(new HealthQueue()) + ->setModel(new HealthStatus()) + ->setModel(new HealthTime()) + ->setModel(new HealthVersion()) ->setModel(new Identity()) - ->setModel(new Token()) + ->setModel(new Index()) + ->setModel(new Installation()) ->setModel(new JWT()) + ->setModel(new Key()) + ->setModel(new Language()) ->setModel(new Locale()) ->setModel(new LocaleCode()) - ->setModel(new File()) - ->setModel(new Bucket()) - ->setModel(new ResourceToken()) - ->setModel(new Team()) + ->setModel(new Log()) + ->setModel(new MFAChallenge()) + ->setModel(new MFAFactors()) + ->setModel(new MFARecoveryCodes()) + ->setModel(new MFAType()) ->setModel(new Membership()) - ->setModel(new Site()) - ->setModel(new TemplateSite()) - ->setModel(new TemplateFramework()) - ->setModel(new Func()) - ->setModel(new TemplateFunction()) - ->setModel(new TemplateRuntime()) - ->setModel(new TemplateVariable()) - ->setModel(new Installation()) + ->setModel(new Message()) + ->setModel(new Metric()) + ->setModel(new MetricBreakdown()) + ->setModel(new Migration()) + ->setModel(new MigrationFirebaseProject()) + ->setModel(new MigrationReport()) + ->setModel(new MockNumber()) + ->setModel(new ModelDocument()) + ->setModel(new Phone()) + ->setModel(new Platform()) + ->setModel(new Preferences()) + ->setModel(new Project()) + ->setModel(new Provider()) ->setModel(new ProviderRepository()) ->setModel(new ProviderRepositoryFramework()) ->setModel(new ProviderRepositoryRuntime()) - ->setModel(new DetectionFramework()) - ->setModel(new DetectionRuntime()) - ->setModel(new VcsContent()) - ->setModel(new Branch()) - ->setModel(new Runtime()) - ->setModel(new Framework()) - ->setModel(new FrameworkAdapter()) - ->setModel(new Deployment()) - ->setModel(new Execution()) - ->setModel(new Project()) - ->setModel(new Webhook()) - ->setModel(new Key()) - ->setModel(new DevKey()) - ->setModel(new MockNumber()) - ->setModel(new AuthProvider()) - ->setModel(new Platform()) - ->setModel(new Variable()) - ->setModel(new Country()) - ->setModel(new Continent()) - ->setModel(new Language()) - ->setModel(new Currency()) - ->setModel(new Phone()) - ->setModel(new HealthAntivirus()) - ->setModel(new HealthQueue()) - ->setModel(new HealthStatus()) - ->setModel(new HealthCertificate()) - ->setModel(new HealthTime()) - ->setModel(new HealthVersion()) - ->setModel(new Metric()) - ->setModel(new MetricBreakdown()) - ->setModel(new UsageDatabases()) - ->setModel(new UsageDatabase()) - ->setModel(new UsageCollection()) - ->setModel(new UsageUsers()) - ->setModel(new UsageStorage()) - ->setModel(new UsageBuckets()) - ->setModel(new UsageFunctions()) - ->setModel(new UsageFunction()) - ->setModel(new UsageSites()) - ->setModel(new UsageSite()) - ->setModel(new UsageProject()) - ->setModel(new Headers()) - ->setModel(new Specification()) + ->setModel(new ResourceToken()) ->setModel(new Rule()) - ->setModel(new TemplateSMS()) - ->setModel(new TemplateEmail()) - ->setModel(new ConsoleVariables()) - ->setModel(new MFAChallenge()) - ->setModel(new MFARecoveryCodes()) - ->setModel(new MFAType()) - ->setModel(new MFAFactors()) - ->setModel(new Provider()) - ->setModel(new Message()) - ->setModel(new Topic()) + ->setModel(new Runtime()) + ->setModel(new Session()) + ->setModel(new Site()) + ->setModel(new Specification()) ->setModel(new Subscriber()) ->setModel(new Target()) - ->setModel(new Migration()) - ->setModel(new MigrationReport()) - ->setModel(new MigrationFirebaseProject()) + ->setModel(new Team()) + ->setModel(new TemplateEmail()) + ->setModel(new TemplateFramework()) + ->setModel(new TemplateFunction()) + ->setModel(new TemplateRuntime()) + ->setModel(new TemplateSMS()) + ->setModel(new TemplateSite()) + ->setModel(new TemplateVariable()) + ->setModel(new Token()) + ->setModel(new Topic()) + ->setModel(new UsageBuckets()) + ->setModel(new UsageCollection()) + ->setModel(new UsageDatabase()) + ->setModel(new UsageDatabases()) + ->setModel(new UsageFunction()) + ->setModel(new UsageFunctions()) + ->setModel(new UsageProject()) + ->setModel(new UsageSite()) + ->setModel(new UsageSites()) + ->setModel(new UsageStorage()) + ->setModel(new UsageUsers()) + ->setModel(new User()) + ->setModel(new Variable()) + ->setModel(new VcsContent()) + ->setModel(new Webhook()) // Tests (keep last) ->setModel(new Mock()); diff --git a/src/Appwrite/Utopia/Response/Model/Transaction.php b/src/Appwrite/Utopia/Response/Model/Transaction.php new file mode 100644 index 0000000000..c6eafd18b9 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Transaction.php @@ -0,0 +1,54 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Transaction ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Transaction creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Transaction update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('status', [ + 'type' => self::TYPE_STRING, + 'description' => 'Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.', + 'default' => 'pending', + 'example' => 'pending', + ]) + ->addRule('expiresAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Expiration time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]); + } + + public function getName(): string + { + return 'Transaction'; + } + + public function getType(): string + { + return Response::MODEL_TRANSACTION; + } +} From cfc6d5fe2b23f80a19ff194616ed5d886a086b06 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:02:32 -0400 Subject: [PATCH 005/145] Add txn collections --- app/config/collections/projects.php | 148 ++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 48a0938a1c..5e0b1b17b5 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2510,4 +2510,152 @@ 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 | rolled_back | failed + 'signed' => true, + 'required' => false, + 'default' => 'pending', + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('expiresAt'), + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => ID::custom('committedAt'), + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => ID::custom('rolledBackAt'), + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_status'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['status'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_expires'), + '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 + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('data'), + 'type' => Database::VAR_STRING, + 'size' => 65535, + 'signed' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => ['json'], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_transaction'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['transactionInternalId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_db_coll'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['databaseId', 'collectionId'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], ]; From 4e2727292eb35333a1051f98f7cf6ba5d6dd135e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:03:33 -0400 Subject: [PATCH 006/145] Add txn routes --- app/controllers/api/databases.php | 233 ++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 0e85171772..31b0585a7e 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1536,6 +1536,239 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); }); +App::post('/v1/databases/transactions') + ->desc('Create transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'createTransaction', + description: '/docs/references/databases/create-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('ttl', 300, new Integer(), 'Seconds before the transaction expires.', true) + ->inject('response') + ->inject('dbForProject') + ->action(function (int $ttl, Response $response, Database $dbForProject) { + $transaction = $dbForProject->createDocument('transactions', new Document([ + '$id' => ID::unique(), + 'status' => 'pending', + 'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl), + ])); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($transaction, Response::MODEL_TRANSACTION); + }); + +App::post('/v1/databases/transactions/:transactionId/operations') + ->desc('Add operations to transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'createOperations', + description: '/docs/references/databases/create-operations.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('operations', [], new ArrayList(new Operation()), 'Array of staged operations.', true) + ->inject('response') + ->inject('dbForProject') + ->action(function (string $transactionId, array $operations, Response $response, Database $dbForProject) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + + if ($transaction->isEmpty() || $transaction['status'] !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + $staged = []; + foreach ($operations as $op) { + $staged[] = new Document([ + '$id' => ID::unique(), + 'transactionId' => $transactionId, + 'databaseId' => $op['databaseId'] ?? null, + 'collectionId' => $op['collectionId'] ?? null, + 'documentId' => $op['documentId'] ?? null, + 'action' => $op['action'], + 'data' => $op['data'] ?? [], + ]); + } + + $dbForProject->createDocuments('transactionLogs', $staged); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($transaction, Response::MODEL_TRANSACTION); + }); + +App::get('/v1/databases/transactions/:transactionId') + ->desc('Get transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'getTransaction', + description: '/docs/references/databases/get-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $transactionId, Response $response, Database $dbForProject) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($transaction, Response::MODEL_TRANSACTION); + }); + +App::patch('/v1/databases/transactions/:transactionId') + ->desc('Update transaction (commit / rollback)') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'updateTransaction', + description: '/docs/references/databases/update-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('action', '', new WhiteList(['commit','rollback']), 'Action to take, commit or rollback.') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->action(function (string $transactionId, string $action, ?string $reason, Response $response, Database $dbForProject, Document $project) { + $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); + } + + switch ($action) { + case 'commit': + // Get staged operations + $operations = $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc('$sequence'), + ]); + + $creates = $updates = $deletes = []; + + foreach ($operations as $operation) { + $databaseId = $operation['databaseInternalId']; + $collectionId = $operation['collectionInternalId']; + $documentId = $operation['documentInternalId']; + + switch ($operation['action']) { + case 'create': + $creates[$databaseId][$collectionId][] = new Document([ + '$id' => $documentId ?? ID::unique(), + ...$operation['data'] + ]); + break; + case 'update': + case 'upsert': + $updates[$databaseId][$collectionId][] = new Document([ + '$id' => $documentId, + ...$operation['data'], + ]); + break; + case 'delete': + $deletes[$databaseId][$collectionId][] = $documentId; + break; + } + } + + unset($databaseId, $collectionId); + + $dbForProject->withTransaction(function () use ($dbForProject, $creates, $updates, $deletes) { + foreach ($creates as $databaseId => $collectionDocs) { + foreach ($collectionDocs as $collectionId => $docs) { + $dbForProject->createDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + } + } + foreach ($updates as $databaseId => $collectionDocs) { + foreach ($collectionDocs as $collectionId => $docs) { + $dbForProject->updateDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + } + } + foreach ($deletes as $databaseId => $collectionDocs) { + foreach ($collectionDocs as $collectionId => $docs) { + $dbForProject->deleteDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + } + } + }); + + $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committed', + ])); + + break; + + case 'rollback': + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc('$sequence'), + ]); + + $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'rolled_back', + 'rolledBackAt' => DateTime::now(), + 'reason' => $reason ?? 'user_request', + ])); + break; + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($transaction, Response::MODEL_TRANSACTION); + }); + App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url') ->desc('Create URL attribute') From 705e5dd45bbf8c3292ca284abab2e7a19f3391f3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:03:58 -0400 Subject: [PATCH 007/145] Add txn support to existing routes --- app/controllers/api/databases.php | 601 +++++++++++++++++++++--------- 1 file changed, 422 insertions(+), 179 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 31b0585a7e..269018033a 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Parameter; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\CustomId; +use Appwrite\Utopia\Database\Validator\Operation; use Appwrite\Utopia\Database\Validator\Queries\Attributes; use Appwrite\Utopia\Database\Validator\Queries\Collections; use Appwrite\Utopia\Database\Validator\Queries\Databases; @@ -3492,17 +3493,18 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ] ) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('documentId', '', new CustomId(), 'Document ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.') + ->param('documentId', '', new CustomId(), 'Document ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->param('data', [], new JSON(), 'Document data as JSON object.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('response') ->inject('dbForProject') ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, ?string $documentId, string $collectionId, string|array|null $data, ?array $permissions, ?array $documents, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, ?string $documentId, string|array|null $data, ?array $permissions, ?array $documents, ?string $transactionId, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $data = \is_string($data) ? \json_decode($data, true) : $data; @@ -3565,6 +3567,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for collections with relationship attributes'); } + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + } + $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) { $allowedPermissions = [ Database::PERMISSION_READ, @@ -3727,26 +3739,46 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') return $document; }, $documents); - try { - $dbForProject->createDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documents - ); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (NotFoundException) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (RelationshipException $e) { - throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } + if (!empty($transactionId)) { + $operations = []; + foreach ($documents as $document) { + $operations[] = new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'create', + 'documentId' => $document->getId(), + 'data' => $document->getArrayCopy(), + ]); + } - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collection->getId()) - ->setContext('collection', $collection) - ->setContext('database', $database); + try { + $dbForProject->createDocuments('transactionLogs', $operations); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (NotFoundException) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } + } else { + try { + $dbForProject->createDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documents + ); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (NotFoundException) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } + } // Add $collectionId and $databaseId for all documents $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { @@ -3786,12 +3818,20 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $processDocument($collection, $document); } + $response->setStatusCode(Response::STATUS_CODE_CREATED); + + if (empty($transactionId)) { + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collection->getId()) + ->setContext('collection', $collection) + ->setContext('database', $database); + } + $queueForStatsUsage ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection - $response->setStatusCode(Response::STATUS_CODE_CREATED); - if ($isBulk) { $response->dynamic(new Document([ 'total' => count($documents), @@ -3801,9 +3841,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') return; } - $queueForEvents - ->setParam('documentId', $documents[0]->getId()) - ->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].create'); + if (empty($transactionId)) { + $queueForEvents + ->setParam('documentId', $documents[0]->getId()) + ->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].create'); + } $response->dynamic( $documents[0], @@ -4244,12 +4286,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->param('documentId', '', new UID(), 'Document ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data) && \is_null($permissions)) { @@ -4269,6 +4312,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::COLLECTION_NOT_FOUND); } + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + } + // Read permission should not be required for update $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); if ($document->isEmpty()) { @@ -4388,20 +4441,31 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); - try { - $document = $dbForProject->updateDocument( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $document->getId(), - $newDocument - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (RelationshipException $e) { - throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + if (!empty($transactionId)) { + $dbForProject->createDocument('transactionLog', new Document([ + 'transactionInternalId' => $transaction->getSequence(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'documentId' => $document->getId(), + 'data' => $newDocument->getArrayCopy(), + 'action' => 'update', + ])); + } else { + try { + $document = $dbForProject->updateDocument( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $document->getId(), + $newDocument + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } } // Add $collectionId and $databaseId for all documents @@ -4447,13 +4511,15 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ) ); - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collection->getId()) - ->setParam('documentId', $document->getId()) - ->setContext('collection', $collection) - ->setContext('database', $database) - ->setPayload($response->getPayload(), sensitive: $relationships); + if (empty($transactionId)) { + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collection->getId()) + ->setParam('documentId', $document->getId()) + ->setContext('collection', $collection) + ->setContext('database', $database) + ->setPayload($response->getPayload(), sensitive: $relationships); + } $response->dynamic($document, Response::MODEL_DOCUMENT); }); @@ -4488,12 +4554,13 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->param('documentId', '', new CustomId(), 'Document ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data) && \is_null($permissions)) { @@ -4513,6 +4580,16 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen throw new Exception(Exception::COLLECTION_NOT_FOUND); } + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + } + // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions, [ Database::PERMISSION_READ, @@ -4622,26 +4699,36 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); - $upserted = []; - try { - $modified = $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - [$newDocument], - onNext: function (Document $document) use (&$upserted) { - $upserted[] = $document; - }, - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (RelationshipException $e) { - throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + if (!empty($transactionId)) { + $dbForProject->createDocument('transactionLog', new Document([ + 'transactionInternalId' => $transaction->getSequence(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'action' => 'update', + 'data' => $newDocument->getArrayCopy(), + ])); + + $upserted = $newDocument; + } else { + try { + $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + [$newDocument], + onNext: function (Document $document) use (&$upserted) { + $upserted = $document; + }, + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } } - $document = $upserted[0]; // Add $collectionId and $databaseId for all documents $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { $document->setAttribute('$databaseId', $database->getId()); @@ -4675,7 +4762,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } }; - $processDocument($collection, $document); + $processDocument($collection, $upserted); $relationships = \array_map( fn ($document) => $document->getAttribute('key'), @@ -4685,15 +4772,17 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ) ); - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collection->getId()) - ->setParam('documentId', $document->getId()) - ->setContext('collection', $collection) - ->setContext('database', $database) - ->setPayload($response->getPayload(), sensitive: $relationships); + if (!empty($transactionId)) { + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collection->getId()) + ->setParam('documentId', $upserted->getId()) + ->setContext('collection', $collection) + ->setContext('database', $database) + ->setPayload($response->getPayload(), sensitive: $relationships); + } - $response->dynamic($document, Response::MODEL_DOCUMENT); + $response->dynamic($upserted, Response::MODEL_DOCUMENT); }); App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/:attribute/increment') @@ -4727,11 +4816,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->param('attribute', '', new Key(), 'Attribute key.') ->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true) ->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -4742,33 +4832,60 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::COLLECTION_NOT_FOUND); } - try { - $document = $dbForProject->increaseDocumentAttribute( - collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - id: $documentId, - attribute: $attribute, - value: $value, - max: $max - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute "' . $attribute . '" has reached the maximum value of ' . $max); - } catch (TypeException) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Attribute "' . $attribute . '" is not a number'); + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + + $dbForProject->createDocument('transactionLog', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'increment', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'max' => $max, + ] + ])); + + $document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId); + } else { + try { + $document = $dbForProject->increaseDocumentAttribute( + collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + id: $documentId, + attribute: $attribute, + value: $value, + max: $max + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (NotFoundException) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute "' . $attribute . '" has reached the maximum value of ' . $max); + } catch (TypeException) { + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Attribute "' . $attribute . '" is not a number'); + } } $queueForStatsUsage ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collectionId) - ->setContext('collection', $collection) - ->setContext('database', $database); + if (!empty($transactionId)) { + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collectionId) + ->setContext('collection', $collection) + ->setContext('database', $database); + } $response->dynamic($document, Response::MODEL_DOCUMENT); }); @@ -4804,11 +4921,24 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->param('attribute', '', new Key(), 'Attribute key.') ->param('value', 1, new Numeric(), 'Value to decrement the attribute by. The value must be a number.', true) ->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function ( + string $databaseId, + string $collectionId, + string $documentId, + string $attribute, + int|float $value, + int|float|null $min, + ?string $transactionId, + Response $response, + Database $dbForProject, + Event $queueForEvents, + StatsUsage $queueForStatsUsage + ) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -4819,33 +4949,64 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::COLLECTION_NOT_FOUND); } - try { - $document = $dbForProject->decreaseDocumentAttribute( - collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - id: $documentId, - attribute: $attribute, - value: $value, - min: $min + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + + $dbForProject->createDocument('transactionLog', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'decrement', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'min' => $min, + ], + ])); + + // Fetch current document for response without mutating + $document = $dbForProject->getDocument( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documentId ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute "' . $attribute . '" has reached the minimum value of ' . $min); - } catch (TypeException) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Attribute "' . $attribute . '" is not a number'); + } else { + try { + $document = $dbForProject->decreaseDocumentAttribute( + collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + id: $documentId, + attribute: $attribute, + value: $value, + min: $min + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (NotFoundException) { + throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute "' . $attribute . '" has reached the minimum value of ' . $min); + } catch (TypeException) { + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Attribute "' . $attribute . '" is not a number'); + } } $queueForStatsUsage ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collectionId) - ->setContext('collection', $collection) - ->setContext('database', $database); + if (empty($transactionId)) { + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collectionId) + ->setContext('collection', $collection) + ->setContext('database', $database); + } $response->dynamic($document, Response::MODEL_DOCUMENT); }); @@ -4878,12 +5039,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true) ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $databaseId, string $collectionId, string|array $data, array $queries, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { + ->action(function (string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { $data = \is_string($data) ? \json_decode($data, true) : $data; @@ -4911,6 +5073,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for collections with relationship attributes'); } + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + } + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -4987,11 +5159,12 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of document data as JSON objects. May contain partial documents.', false, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $databaseId, string $collectionId, array $documents, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { + ->action(function (string $databaseId, string $collectionId, array $documents, ?string $transactionId, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -5015,26 +5188,52 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') $documents[$key] = new Document($document); } - $upserted = []; + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } - try { - $modified = $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documents, - onNext: function (Document $document) use ($plan, &$upserted) { - if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $upserted[] = $document; - } - }, - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (RelationshipException $e) { - throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + $operations = []; + foreach ($documents as $doc) { + $operations[] = new Document([ + 'transactionInternalId' => $transaction->getSequence(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'action' => 'upsert', + 'data' => $doc->getArrayCopy(), + ]); + } + + $dbForProject->createDocuments('transactionLogs', $operations); + + $modified = \count($documents); + $upserted = $documents; + } else { + $upserted = []; + + try { + $modified = $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documents, + onNext: function (Document $document) use ($plan, &$upserted) { + if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $upserted[] = $document; + } + }, + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } } foreach ($upserted as $document) { @@ -5081,12 +5280,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', '', new UID(), 'Document ID.') + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.') ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -5106,15 +5306,33 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu throw new Exception(Exception::DOCUMENT_NOT_FOUND); } - try { - $dbForProject->deleteDocument( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documentId - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (RestrictedException) { - throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + + $dbForProject->createDocument('transactionLog', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $document->getId(), + 'action' => 'delete', + ])); + } else { + try { + $dbForProject->deleteDocument( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documentId + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); + } } $operations = 0; @@ -5160,21 +5378,23 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); - $relationships = \array_map( - fn ($document) => $document->getAttribute('key'), - \array_filter( - $collection->getAttribute('attributes', []), - fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP - ) - ); + if (!empty($transactionId)) { + $relationships = \array_map( + fn ($document) => $document->getAttribute('key'), + \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + ) + ); - $queueForEvents - ->setParam('databaseId', $databaseId) - ->setParam('collectionId', $collection->getId()) - ->setParam('documentId', $document->getId()) - ->setContext('collection', $collection) - ->setContext('database', $database) - ->setPayload($response->output($document, Response::MODEL_DOCUMENT), sensitive: $relationships); + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collection->getId()) + ->setParam('documentId', $document->getId()) + ->setContext('collection', $collection) + ->setContext('database', $database) + ->setPayload($response->output($document, Response::MODEL_DOCUMENT), sensitive: $relationships); + } $response->noContent(); }); @@ -5206,12 +5426,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $databaseId, string $collectionId, array $queries, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { + ->action(function (string $databaseId, string $collectionId, array $queries, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -5231,6 +5452,16 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for collections with relationship attributes'); } + if (!empty($transactionId)) { + $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_INVALID, 'Transaction is not pending'); + } + } + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -5239,20 +5470,32 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') $documents = []; - try { - $modified = $dbForProject->deleteDocuments( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $queries, - onNext: function (Document $document) use ($plan, &$documents) { - if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $documents[] = $document; - } - }, - ); - } catch (ConflictException) { - throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (RestrictedException) { - throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); + if (!empty($transactionId)) { + $dbForProject->createDocument('transactionLog', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'delete', + 'data' => [$queries], + ])); + + $modified = 0; + } else { + try { + $modified = $dbForProject->deleteDocuments( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $queries, + onNext: function (Document $document) use ($plan, &$documents) { + if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $documents[] = $document; + } + }, + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); + } } foreach ($documents as $document) { From b71688a587e62f71da6f845f967fd139f7266114 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:07:14 -0400 Subject: [PATCH 008/145] Format --- app/controllers/api/databases.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 269018033a..534b2fbe04 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -5199,7 +5199,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') $operations = []; foreach ($documents as $doc) { - $operations[] = new Document([ + $operations[] = new Document([ 'transactionInternalId' => $transaction->getSequence(), 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), From d13081b4f29c49ad8ae418b81605d67275b8181a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:45:07 -0400 Subject: [PATCH 009/145] Use const --- app/controllers/api/databases.php | 2 +- app/init/constants.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 534b2fbe04..9175b53f19 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1556,7 +1556,7 @@ App::post('/v1/databases/transactions') ], contentType: ContentType::JSON )) - ->param('ttl', 300, new Integer(), 'Seconds before the transaction expires.', true) + ->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true) ->inject('response') ->inject('dbForProject') ->action(function (int $ttl, Response $response, Database $dbForProject) { diff --git a/app/init/constants.php b/app/init/constants.php index ebf79086a7..96606ff782 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -52,6 +52,9 @@ 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_STORAGE_UPLOADS = '/storage/uploads'; const APP_STORAGE_SITES = '/storage/sites'; const APP_STORAGE_FUNCTIONS = '/storage/functions'; From 4da95f765b44b5b9592e5152d4fb7e7783c38b72 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:45:14 -0400 Subject: [PATCH 010/145] Don't require data --- src/Appwrite/Utopia/Database/Validator/Operation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 3f9a15673a..e46862bdb2 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -38,7 +38,7 @@ class Operation extends Validator } // Mandatory keys - $required = ['databaseId', 'collectionId', 'action', 'payload']; + $required = ['databaseId', 'collectionId', 'action']; foreach ($required as $key) { if (!\array_key_exists($key, $value)) { $this->description = "Missing required key: {$key}"; From bd9d827a16b0e665ee7faeca7a52c7d7c176f46b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:54:32 -0400 Subject: [PATCH 011/145] Add tests --- .../e2e/Services/Databases/DatabasesBase.php | 593 ++++++++++++++++-- 1 file changed, 555 insertions(+), 38 deletions(-) diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index e34691839d..898996733c 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -3891,28 +3891,47 @@ trait DatabasesBase $this->assertCount(1, $documentsUser2['body']['documents']); } - /** - * @depends testDefaultPermissions - */ - public function testUniqueIndexDuplicate(array $data): array + public function testUniqueIndexDuplicate(): void { - $databaseId = $data['databaseId']; - $uniqueIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([ + // Setup: create database, collection, attribute, and initial document + $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'] ]), [ - 'key' => 'unique_title', - 'type' => 'unique', - 'attributes' => ['title'], + 'databaseId' => ID::unique(), + 'name' => 'Unique Index DB', ]); - - $this->assertEquals(202, $uniqueIndex['headers']['status-code']); - + $databaseId = $database['body']['$id']; + $movies = $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' => 'Movies', + 'permissions' => [ + Permission::create(Role::user(ID::custom($this->getUser()['$id']))), + Permission::read(Role::user(ID::custom($this->getUser()['$id']))), + Permission::update(Role::user(ID::custom($this->getUser()['$id']))), + Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), + ], + 'documentSecurity' => true, + ]); + $moviesId = $movies['body']['$id']; + $title = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $title['headers']['status-code']); sleep(2); - - // test for failure - $duplicate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([ + // Insert initial document + $doc1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -3931,11 +3950,45 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); + $this->assertEquals(201, $doc1['headers']['status-code']); + // Create unique index + $uniqueIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'unique_title', + 'type' => 'unique', + 'attributes' => ['title'], + ]); + $this->assertEquals(202, $uniqueIndex['headers']['status-code']); + sleep(2); + + // test for failure (duplicate title) + $duplicate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'title' => 'Captain America', + 'releaseYear' => 1944, + 'actors' => [ + 'Chris Evans', + 'Samuel Jackson', + ] + ], + 'permissions' => [ + Permission::read(Role::user(ID::custom($this->getUser()['$id']))), + Permission::update(Role::user(ID::custom($this->getUser()['$id']))), + Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), + ] + ]); $this->assertEquals(409, $duplicate['headers']['status-code']); // Test for exception when updating document to conflict - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -3954,11 +4007,9 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); - $this->assertEquals(201, $document['headers']['status-code']); - // Test for exception when updating document to conflict - $duplicate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $document['body']['$id'], array_merge([ + $duplicate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $document['body']['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -3977,17 +4028,48 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); - $this->assertEquals(409, $duplicate['headers']['status-code']); - - return $data; } - /** - * @depends testUniqueIndexDuplicate - */ - public function testPersistantCreatedAt(array $data): array + public function testPersistentCreatedAt(): void { + // Setup: create database, collection, attribute + $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' => 'CreatedAtDB', + ]); + $databaseId = $database['body']['$id']; + $movies = $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' => 'Movies', + 'permissions' => [ + Permission::create(Role::user(ID::custom($this->getUser()['$id']))), + Permission::read(Role::user(ID::custom($this->getUser()['$id']))), + Permission::update(Role::user(ID::custom($this->getUser()['$id']))), + Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), + ], + 'documentSecurity' => true, + ]); + $moviesId = $movies['body']['$id']; + $title = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $title['headers']['status-code']); + sleep(2); $headers = $this->getSide() === 'client' ? array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -3997,52 +4079,43 @@ trait DatabasesBase 'x-appwrite-key' => $this->getProject()['apiKey'] ]; - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents', $headers, [ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', $headers, [ 'documentId' => ID::unique(), 'data' => [ 'title' => 'Creation Date Test', 'releaseYear' => 2000 ] ]); - $this->assertEquals($document['body']['title'], 'Creation Date Test'); - $documentId = $document['body']['$id']; $createdAt = $document['body']['$createdAt']; $updatedAt = $document['body']['$updatedAt']; \sleep(1); - - $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [ + $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $documentId, $headers, [ 'data' => [ 'title' => 'Updated Date Test', ] ]); - $updatedAtSecond = $document['body']['$updatedAt']; - $this->assertEquals($document['body']['title'], 'Updated Date Test'); $this->assertEquals($document['body']['$createdAt'], $createdAt); $this->assertNotEquals($document['body']['$updatedAt'], $updatedAt); \sleep(1); - - $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [ + $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $documentId, $headers, [ 'data' => [ 'title' => 'Again Updated Date Test', '$createdAt' => '2022-08-01 13:09:23.040', // $createdAt is not updatable '$updatedAt' => '2022-08-01 13:09:23.050' // system will update it not api ] ]); - $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); $this->assertEquals($document['body']['$createdAt'], $createdAt); $this->assertNotEquals($document['body']['$createdAt'], '2022-08-01 13:09:23.040'); $this->assertNotEquals($document['body']['$updatedAt'], $updatedAt); $this->assertNotEquals($document['body']['$updatedAt'], $updatedAtSecond); $this->assertNotEquals($document['body']['$updatedAt'], '2022-08-01 13:09:23.050'); - - return $data; } public function testUpdatePermissionsWithEmptyPayload(): array @@ -5506,5 +5579,449 @@ trait DatabasesBase $this->assertEquals(400, $typeErr['headers']['status-code']); } + public function testCreateTransaction(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionTestDatabase' + ]); + $databaseId = $database['body']['$id']; + + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 900 + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertArrayHasKey('transactionId', $response['body']); + $this->assertEquals('pending', $response['body']['status']); + $this->assertArrayHasKey('expiresAt', $response['body']); + $this->assertGreaterThanOrEqual(DateTime::addSeconds(new \DateTime(), 800), strtotime($response['body']['expiresAt'])); + $this->assertLessThanOrEqual(DateTime::addSeconds(new \DateTime(), 1000), strtotime($response['body']['expiresAt'])); + + // Failure: invalid TTL + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => -1 + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 1000000 + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testAddOperationsToTransaction(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionTestDatabase' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $transaction = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 300 + ]); + + $transactionId = $transaction['body']['transactionId']; + + // Add operations + $transaction = $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => [ + 'name' => 'Tester 1' + ], + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => [ + 'name' => 'Tester 2' + ], + ] + ] + ]); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $this->assertNotEmpty($transaction['body']); + + // Failure cases + + // Invalid databaseId + $response = $this->client->call(Client::METHOD_POST, "/databases/invalidDatabaseId/transactions/$transactionId/operations", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'operations' => [ + [ + 'databaseId' => 'invalidDatabaseId', + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => ['name' => 'Invalid Tester'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Invalid collectionId + $response = $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => 'invalidCollectionId', + 'action' => 'create', + 'data' => ['name' => 'Invalid Tester'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Invalid action + $response = $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'invalidAction', + 'data' => ['name' => 'Invalid Tester'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Missing required fields + $response = $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId + // Missing action + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testCommitTransaction(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionTestDatabase' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionTestCommit', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + \sleep(2); + + $transaction = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $transactionId = $transaction['body']['transactionId']; + + $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => ['name' => 'Tester'], + ] + ] + ]); + + $transaction = $this->client->call(Client::METHOD_PATCH, "/databases/$databaseId/transactions/$transactionId", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'commit' => true + ]); + + $this->assertEquals(200, $transaction['headers']['status-code']); + $this->assertEquals('committed', $transaction['body']['status']); + + $documents = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(1, count($documents['body']['documents'])); + $this->assertEquals('Tester', $documents['body']['documents'][0]['name']); + } + + public function testRollbackTransaction(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionTestDatabase' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionTestRollback', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + \sleep(2); + + $transaction = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $transactionId = $transaction['body']['transactionId']; + + $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => [ + 'name' => 'value' + ], + ] + ] + ]); + + $rollback = $this->client->call(Client::METHOD_PATCH, "/databases/$databaseId/transactions/$transactionId", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $rollback['headers']['status-code']); + $this->assertEquals('rolledBack', $rollback['body']['status']); + + $documents = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents", [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(0, count($documents['body']['documents'])); + } + public function testTransactionConflict(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionTestConflictDatabase' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionTestConflict', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + + $collectionId = $collection['body']['$id']; + + // Add string attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], $this->getHeaders()), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $transaction = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/transactions', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $transactionId = $transaction['body']['transactionId']; + + $this->client->call(Client::METHOD_POST, "/databases/$databaseId/transactions/$transactionId/operations", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => ['attribute' => 'value'], + ] + ] + ]); + + // Commit the transaction + $this->client->call(Client::METHOD_PATCH, "/databases/$databaseId/transactions/$transactionId", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'commit' => true + ]); + + // Attempt to commit again, should fail with a conflict + $conflictResponse = $this->client->call(Client::METHOD_PATCH, "/databases/$databaseId/transactions/$transactionId", \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'commit' => true + ]); + + $this->assertEquals(409, $conflictResponse['headers']['status-code']); + } } From 145cb54bc034655bdeb912188d3387eab20a9761 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 16:58:25 -0400 Subject: [PATCH 012/145] Separate bulk --- app/config/collections/projects.php | 4 +-- app/controllers/api/databases.php | 30 ++++++++++++------- .../Utopia/Database/Validator/Operation.php | 26 ++++++++++------ src/Appwrite/Utopia/Response.php | 2 ++ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 97cde969de..7a1465fb92 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2651,9 +2651,9 @@ return [ 'orders' => [], ], [ - '$id' => ID::custom('_key_db_coll'), + '$id' => ID::custom('_key_internal_path'), 'type' => Database::INDEX_KEY, - 'attributes' => ['databaseId', 'collectionId'], + 'attributes' => ['databaseInternalId', 'collectionInternalId'], 'lengths' => [], 'orders' => [], ], diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 9175b53f19..0159b199c7 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -4442,10 +4442,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLog', new Document([ - 'transactionInternalId' => $transaction->getSequence(), + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), 'documentId' => $document->getId(), 'data' => $newDocument->getArrayCopy(), 'action' => 'update', @@ -4700,10 +4700,10 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLog', new Document([ - 'transactionInternalId' => $transaction->getSequence(), + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), 'action' => 'update', 'data' => $newDocument->getArrayCopy(), ])); @@ -4841,7 +4841,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLog', new Document([ + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), @@ -4958,7 +4958,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLog', new Document([ + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), @@ -5098,6 +5098,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') $documents = []; + if (!empty($transactionId)) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'bulkUpdate', + 'data' => \compact('data', 'queries'), + ])); + } + try { $modified = $dbForProject->updateDocuments( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), @@ -5315,7 +5325,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLog', new Document([ + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), @@ -5471,12 +5481,12 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') $documents = []; if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLog', new Document([ + $dbForProject->createDocument('transactionLogs', new Document([ 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'action' => 'delete', - 'data' => [$queries], + 'action' => 'bulkDelete', + 'data' => ['queries' => $queries], ])); $modified = 0; diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index e46862bdb2..45122df800 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -8,12 +8,21 @@ class Operation extends Validator { private string $description = ''; - /** @var string[] */ + /** @var array */ + private array $required = [ + 'databaseId', + 'collectionId', + 'action', + ]; + + /** @var array */ private array $actions = [ 'create', 'update', + 'bulkUpdate', 'upsert', - 'delete' + 'delete', + 'bulkDelete', ]; public function getDescription(): string @@ -38,16 +47,15 @@ class Operation extends Validator } // Mandatory keys - $required = ['databaseId', 'collectionId', 'action']; - foreach ($required as $key) { + foreach ($this->required as $key) { if (!\array_key_exists($key, $value)) { $this->description = "Missing required key: {$key}"; return false; } } - // databaseId / collectionId / action must be non‑empty strings - foreach (['databaseId', 'collectionId', 'action'] as $key) { + // Required keys must be non‑empty + foreach ($this->required as $key) { if (!\is_string($value[$key]) || \trim($value[$key]) === '') { $this->description = "Key '{$key}' must be a non‑empty string"; return false; @@ -60,9 +68,9 @@ class Operation extends Validator return false; } - // Payload must be array (can be empty) - if (!\is_array($value['payload'])) { - $this->description = "Key 'payload' must be an array"; + // Data must be array (can be empty) + if (!\is_array($value['data'])) { + $this->description = "Key 'data' must be an array"; return false; } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index a376dd9cb6..994f0e2e73 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -105,6 +105,7 @@ use Appwrite\Utopia\Response\Model\TemplateSMS; use Appwrite\Utopia\Response\Model\TemplateVariable; use Appwrite\Utopia\Response\Model\Token; use Appwrite\Utopia\Response\Model\Topic; +use Appwrite\Utopia\Response\Model\Transaction; use Appwrite\Utopia\Response\Model\UsageBuckets; use Appwrite\Utopia\Response\Model\UsageCollection; use Appwrite\Utopia\Response\Model\UsageDatabase; @@ -515,6 +516,7 @@ class Response extends SwooleResponse ->setModel(new TemplateVariable()) ->setModel(new Token()) ->setModel(new Topic()) + ->setModel(new Transaction()) ->setModel(new UsageBuckets()) ->setModel(new UsageCollection()) ->setModel(new UsageDatabase()) From 0b00ce28e0c241cba4642abf00905551c4c34010 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 19:37:53 -0400 Subject: [PATCH 013/145] Remove redundant attributes --- app/config/collections/projects.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 7a1465fb92..e76a17abe5 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2537,26 +2537,6 @@ return [ 'array' => false, 'filters' => ['datetime'], ], - [ - '$id' => ID::custom('committedAt'), - 'type' => Database::VAR_DATETIME, - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], - ], - [ - '$id' => ID::custom('rolledBackAt'), - 'type' => Database::VAR_DATETIME, - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], - ], ], 'indexes' => [ [ From 1e82c7f0ef37d4e9b6a37ee1126757a26b9a6036 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 21:54:30 -0400 Subject: [PATCH 014/145] Add listTransactions --- app/controllers/api/databases.php | 35 +++++++++++++++++++ .../Utopia/Database/Validator/Operation.php | 2 +- .../Validator/Queries/Transactions.php | 16 +++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 0159b199c7..9819377f9d 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1621,6 +1621,41 @@ App::post('/v1/databases/transactions/:transactionId/operations') ->dynamic($transaction, Response::MODEL_TRANSACTION); }); +App::get('/v1/databases/transactions') + ->desc('List transactions') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'listTransactions', + description: '/docs/references/databases/list-transactions.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_TRANSACTION_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->param('queries', [], new Transactions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries).', true) + ->inject('response') + ->inject('dbForProject') + ->action(function (array $queries, Response $response, Database $dbForProject) { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $response->dynamic(new Document([ + 'transactions' => $dbForProject->find('transactions', $queries), + 'total' => $dbForProject->count('transactions', $queries), + ]), Response::MODEL_TRANSACTION_LIST); + }); + App::get('/v1/databases/transactions/:transactionId') ->desc('Get transaction') ->groups(['api', 'database', 'transactions']) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 45122df800..989fd76eec 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -19,9 +19,9 @@ class Operation extends Validator private array $actions = [ 'create', 'update', - 'bulkUpdate', 'upsert', 'delete', + 'bulkUpdate', 'bulkDelete', ]; diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php new file mode 100644 index 0000000000..ab3e933d6f --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php @@ -0,0 +1,16 @@ + Date: Tue, 17 Jun 2025 21:54:40 -0400 Subject: [PATCH 015/145] Update params --- app/controllers/api/databases.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 9819377f9d..69537f434f 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1691,7 +1691,7 @@ App::get('/v1/databases/transactions/:transactionId') }); App::patch('/v1/databases/transactions/:transactionId') - ->desc('Update transaction (commit / rollback)') + ->desc('Update transaction') ->groups(['api', 'database', 'transactions']) ->label('scope', 'collections.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) @@ -1710,11 +1710,20 @@ App::patch('/v1/databases/transactions/:transactionId') contentType: ContentType::JSON )) ->param('transactionId', '', new UID(), 'Transaction ID.') - ->param('action', '', new WhiteList(['commit','rollback']), 'Action to take, commit or rollback.') + ->param('commit', false, new Boolean(), 'Commit transaction?', true) + ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) + ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') ->inject('project') - ->action(function (string $transactionId, string $action, ?string $reason, Response $response, Database $dbForProject, Document $project) { + ->action(function (string $transactionId, bool $commit, bool $rollback, ?\DateTime $requestTimestamp, ?string $reason, Response $response, Database $dbForProject, Document $project) { + if (!$commit && !$rollback) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); + } + if ($commit && $rollback) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); + } + $transaction = $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty()) { From 5d5a6fbc5d89d38e2e7a9508bed186dc0adfd9f5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 21:55:08 -0400 Subject: [PATCH 016/145] Add delete transaction --- app/controllers/api/databases.php | 205 +++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 61 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 69537f434f..9f6cfa1aec 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1734,79 +1734,122 @@ App::patch('/v1/databases/transactions/:transactionId') throw new Exception(Exception::TRANSACTION_NOT_READY); } - switch ($action) { - case 'commit': - // Get staged operations - $operations = $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc('$sequence'), - ]); + $now = new \DateTime(); + $expiresAt = new \DateTime($transaction->getAttribute('expiresAt')); + if ($now > $expiresAt) { + throw new Exception(Exception::TRANSACTION_EXPIRED); + } - $creates = $updates = $deletes = []; + if ($commit) { + $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committing', + ])); - foreach ($operations as $operation) { - $databaseId = $operation['databaseInternalId']; - $collectionId = $operation['collectionInternalId']; - $documentId = $operation['documentInternalId']; + $operations = $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); - switch ($operation['action']) { - case 'create': - $creates[$databaseId][$collectionId][] = new Document([ - '$id' => $documentId ?? ID::unique(), - ...$operation['data'] - ]); - break; - case 'update': - case 'upsert': - $updates[$databaseId][$collectionId][] = new Document([ - '$id' => $documentId, - ...$operation['data'], - ]); - break; - case 'delete': - $deletes[$databaseId][$collectionId][] = $documentId; - break; - } - } + $creates = $updates = $deletes = $bulkUpdates = $bulkDeletes = []; - unset($databaseId, $collectionId); + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $documentId = $operation['documentId']; - $dbForProject->withTransaction(function () use ($dbForProject, $creates, $updates, $deletes) { - foreach ($creates as $databaseId => $collectionDocs) { - foreach ($collectionDocs as $collectionId => $docs) { - $dbForProject->createDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + switch ($operation['action']) { + case 'create': + $creates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId ?? ID::unique(), + ...$operation['data'] + ]); + break; + case 'update': + case 'upsert': + $updates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId, + ...$operation['data'], + ]); + break; + case 'delete': + $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; + break; + case 'bulkUpdate': + $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ + 'data' => $operation['data'] ?? null, + 'queries' => $operation['queries'] ?? [], + ]; + break; + case 'bulkDelete': + $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ + 'queries' => $operation['queries'] ?? [], + ]; + break; } } - foreach ($updates as $databaseId => $collectionDocs) { - foreach ($collectionDocs as $collectionId => $docs) { - $dbForProject->updateDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + + try { + foreach ($creates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createDocuments("database_{$dbId}_collection_{$colId}", $docs); + } } - } - foreach ($deletes as $databaseId => $collectionDocs) { - foreach ($collectionDocs as $collectionId => $docs) { - $dbForProject->deleteDocuments("database_{$databaseId}_collection_{$collectionId}", $docs); + foreach ($updates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createOrUpdateDocuments("database_{$dbId}_collection_{$colId}", $docs); + } } + foreach ($deletes as $dbId => $cols) { + foreach ($cols as $colId => $ids) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", [ + Query::equal('$id', $ids), + ]); + } + } + foreach ($bulkUpdates as $dbId => $cols) { + foreach ($cols as $colId => $updates) { + foreach ($updates as $update) { + $dbForProject->updateDocuments("database_{$dbId}_collection_{$colId}", $update['data'], $update['queries']); + } + } + } + foreach ($bulkDeletes as $dbId => $cols) { + foreach ($cols as $colId => $deletes) { + foreach ($deletes as $delete) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", $delete['queries']); + } + } + } + + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committed', + ])); + + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + } catch (DuplicateException|ConflictException) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + + throw new Exception(Exception::TRANSACTION_CONFLICT); } }); + }); - $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committed', - ])); + $transaction = $dbForProject->getDocument('transactions', $transactionId); + } - break; + if ($rollback) { + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); - case 'rollback': - $dbForProject->deleteDocuments('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc('$sequence'), - ]); - - $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'rolled_back', - 'rolledBackAt' => DateTime::now(), - 'reason' => $reason ?? 'user_request', - ])); - break; + $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'rolledBack', + ])); } $response @@ -1814,6 +1857,46 @@ App::patch('/v1/databases/transactions/:transactionId') ->dynamic($transaction, Response::MODEL_TRANSACTION); }); +App::delete('/v1/databases/transactions/:transactionId') + ->desc('Delete transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'deleteTransaction', + description: '/docs/references/databases/delete-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_NOCONTENT, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $transactionId, Response $response, Database $dbForProject) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('transactions', $transactionId)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove transaction from DB'); + } + + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $response->noContent(); + }); + App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->alias('/v1/database/collections/:collectionId/attributes/url') ->desc('Create URL attribute') @@ -3790,8 +3873,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'action' => 'create', 'documentId' => $document->getId(), + 'action' => 'create', 'data' => $document->getArrayCopy(), ]); } @@ -4491,8 +4574,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), 'documentId' => $document->getId(), - 'data' => $newDocument->getArrayCopy(), 'action' => 'update', + 'data' => $newDocument->getArrayCopy(), ])); } else { try { From 75469eed7d6636d93b141e630575ad8336f2106a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 21:55:38 -0400 Subject: [PATCH 017/145] Fix staged values --- app/controllers/api/databases.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 9f6cfa1aec..cc51fd0def 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -18,6 +18,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Attributes; use Appwrite\Utopia\Database\Validator\Queries\Collections; use Appwrite\Utopia\Database\Validator\Queries\Databases; use Appwrite\Utopia\Database\Validator\Queries\Indexes; +use Appwrite\Utopia\Database\Validator\Queries\Transactions; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use MaxMind\Db\Reader; @@ -1594,23 +1595,35 @@ App::post('/v1/databases/transactions/:transactionId/operations') ->param('operations', [], new ArrayList(new Operation()), 'Array of staged operations.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $transactionId, array $operations, Response $response, Database $dbForProject) { + ->inject('plan') + ->action(function (string $transactionId, array $operations, Response $response, Database $dbForProject, array $plan) { $transaction = $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty() || $transaction['status'] !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } - $staged = []; - foreach ($operations as $op) { + + $databases = $collections = $staged = []; + foreach ($operations as $operation) { + $database = $databases[$operation['databaseId']] ??= $dbForProject->getDocument('databases', $operation['databaseId']); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $collection = $collections[$operation['collectionId']] ??= $dbForProject->getDocument('database_' . $database->getSequence(), $operation['collectionId']); + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + $staged[] = new Document([ '$id' => ID::unique(), - 'transactionId' => $transactionId, - 'databaseId' => $op['databaseId'] ?? null, - 'collectionId' => $op['collectionId'] ?? null, - 'documentId' => $op['documentId'] ?? null, - 'action' => $op['action'], - 'data' => $op['data'] ?? [], + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $operation['documentId'] ?? ID::unique(), + 'action' => $operation['action'], + 'data' => $operation['data'] ?? new \stdClass(), ]); } From 910f27016bfb4c3fddd36b06e5f48e7dd6b0ff6c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 17 Jun 2025 22:08:08 -0400 Subject: [PATCH 018/145] Add operation to txn meta --- app/config/collections/projects.php | 10 ++++++++++ app/controllers/api/databases.php | 20 +++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index e76a17abe5..53a665bfab 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2527,6 +2527,16 @@ return [ '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, diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index cc51fd0def..3e0ec7a88b 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1564,6 +1564,7 @@ App::post('/v1/databases/transactions') $transaction = $dbForProject->createDocument('transactions', new Document([ '$id' => ID::unique(), 'status' => 'pending', + 'operations' => 0, 'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl), ])); @@ -1598,11 +1599,19 @@ App::post('/v1/databases/transactions/:transactionId/operations') ->inject('plan') ->action(function (string $transactionId, array $operations, Response $response, Database $dbForProject, array $plan) { $transaction = $dbForProject->getDocument('transactions', $transactionId); - - if ($transaction->isEmpty() || $transaction['status'] !== 'pending') { + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $existing = $transaction->getAttribute('operations', 0); + + if (($existing + \count($operations)) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding ' . \count($operations) . ' would exceed the maximum of ' . $maxBatch + ); + } $databases = $collections = $staged = []; foreach ($operations as $operation) { @@ -1627,7 +1636,12 @@ App::post('/v1/databases/transactions/:transactionId/operations') ]); } - $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'operations' => $existing + \count($operations), + ])); + }); $response ->setStatusCode(Response::STATUS_CODE_CREATED) From b471bd0f581ed06ab886a664d133c8017d141973 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 18 Jun 2025 16:53:47 -0400 Subject: [PATCH 019/145] Add operations to response model --- app/controllers/api/databases.php | 182 +++++++++--------- .../Utopia/Response/Model/Transaction.php | 6 + 2 files changed, 95 insertions(+), 93 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 3e0ec7a88b..5b9d571211 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1752,118 +1752,114 @@ App::patch('/v1/databases/transactions/:transactionId') } $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); } $now = new \DateTime(); - $expiresAt = new \DateTime($transaction->getAttribute('expiresAt')); + $expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now')); if ($now > $expiresAt) { throw new Exception(Exception::TRANSACTION_EXPIRED); } if ($commit) { - $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committing', + ])); + + $operations = $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $creates = $updates = $deletes = $bulkUpdates = $bulkDeletes = []; + + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $documentId = $operation['documentId']; + + switch ($operation['action']) { + case 'create': + $creates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId ?? ID::unique(), + ...$operation['data'] + ]); + break; + case 'update': + case 'upsert': + $updates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId, + ...$operation['data'], + ]); + break; + case 'delete': + $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; + break; + case 'bulkUpdate': + $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ + 'data' => $operation['data'] ?? null, + 'queries' => $operation['queries'] ?? [], + ]; + break; + case 'bulkDelete': + $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ + 'queries' => $operation['queries'] ?? [], + ]; + break; + } + } + + try { + foreach ($creates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createDocuments("database_{$dbId}_collection_{$colId}", $docs); + } + } + foreach ($updates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createOrUpdateDocuments("database_{$dbId}_collection_{$colId}", $docs); + } + } + foreach ($deletes as $dbId => $cols) { + foreach ($cols as $colId => $ids) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", [ + Query::equal('$id', $ids), + ]); + } + } + foreach ($bulkUpdates as $dbId => $cols) { + foreach ($cols as $colId => $updates) { + foreach ($updates as $update) { + $dbForProject->updateDocuments("database_{$dbId}_collection_{$colId}", $update['data'], $update['queries']); + } + } + } + foreach ($bulkDeletes as $dbId => $cols) { + foreach ($cols as $colId => $deletes) { + foreach ($deletes as $delete) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", $delete['queries']); + } + } + } + $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committing', + 'status' => 'committed', ])); - $operations = $dbForProject->find('transactionLogs', [ + $dbForProject->deleteDocuments('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), ]); + } catch (DuplicateException|ConflictException) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); - $creates = $updates = $deletes = $bulkUpdates = $bulkDeletes = []; - - foreach ($operations as $operation) { - $databaseInternalId = $operation['databaseInternalId']; - $collectionInternalId = $operation['collectionInternalId']; - $documentId = $operation['documentId']; - - switch ($operation['action']) { - case 'create': - $creates[$databaseInternalId][$collectionInternalId][] = new Document([ - '$id' => $documentId ?? ID::unique(), - ...$operation['data'] - ]); - break; - case 'update': - case 'upsert': - $updates[$databaseInternalId][$collectionInternalId][] = new Document([ - '$id' => $documentId, - ...$operation['data'], - ]); - break; - case 'delete': - $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; - break; - case 'bulkUpdate': - $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ - 'data' => $operation['data'] ?? null, - 'queries' => $operation['queries'] ?? [], - ]; - break; - case 'bulkDelete': - $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ - 'queries' => $operation['queries'] ?? [], - ]; - break; - } - } - - try { - foreach ($creates as $dbId => $cols) { - foreach ($cols as $colId => $docs) { - $dbForProject->createDocuments("database_{$dbId}_collection_{$colId}", $docs); - } - } - foreach ($updates as $dbId => $cols) { - foreach ($cols as $colId => $docs) { - $dbForProject->createOrUpdateDocuments("database_{$dbId}_collection_{$colId}", $docs); - } - } - foreach ($deletes as $dbId => $cols) { - foreach ($cols as $colId => $ids) { - $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", [ - Query::equal('$id', $ids), - ]); - } - } - foreach ($bulkUpdates as $dbId => $cols) { - foreach ($cols as $colId => $updates) { - foreach ($updates as $update) { - $dbForProject->updateDocuments("database_{$dbId}_collection_{$colId}", $update['data'], $update['queries']); - } - } - } - foreach ($bulkDeletes as $dbId => $cols) { - foreach ($cols as $colId => $deletes) { - foreach ($deletes as $delete) { - $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", $delete['queries']); - } - } - } - - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committed', - ])); - - $dbForProject->deleteDocuments('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - ]); - } catch (DuplicateException|ConflictException) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - - throw new Exception(Exception::TRANSACTION_CONFLICT); - } - }); + throw new Exception(Exception::TRANSACTION_CONFLICT); + } }); $transaction = $dbForProject->getDocument('transactions', $transactionId); diff --git a/src/Appwrite/Utopia/Response/Model/Transaction.php b/src/Appwrite/Utopia/Response/Model/Transaction.php index c6eafd18b9..aae2a9b572 100644 --- a/src/Appwrite/Utopia/Response/Model/Transaction.php +++ b/src/Appwrite/Utopia/Response/Model/Transaction.php @@ -34,6 +34,12 @@ class Transaction extends Model 'default' => 'pending', 'example' => 'pending', ]) + ->addRule('operations', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of operations in the transaction.', + 'default' => 0, + 'example' => 5, + ]) ->addRule('expiresAt', [ 'type' => self::TYPE_DATETIME, 'description' => 'Expiration time in ISO 8601 format.', From ac329479085a94963e93ed45774ed5e42f0a6d79 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 18 Jun 2025 19:17:47 -0400 Subject: [PATCH 020/145] Update db --- composer.json | 2 +- composer.lock | 39 ++++++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 18507e04f1..2529dc404d 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.71.*", + "utopia-php/database": "dev-feat-transaction-pinning as 0.71.6", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dsn": "0.2.1", diff --git a/composer.lock b/composer.lock index ff6884de7d..aee5501bc8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1557e469b3074a6478a0b2fd522e1a2a", + "content-hash": "aabb75770fc1377ff5188957653cbdea", "packages": [ { "name": "adhocore/jwt", @@ -3490,16 +3490,16 @@ }, { "name": "utopia-php/database", - "version": "0.71.6", + "version": "dev-feat-transaction-pinning", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "2bd87acc40af087fc0fdcccc47c43141dff0be5c" + "reference": "8c764ed339caa60687b2362a96712bb6cbabf1eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/2bd87acc40af087fc0fdcccc47c43141dff0be5c", - "reference": "2bd87acc40af087fc0fdcccc47c43141dff0be5c", + "url": "https://api.github.com/repos/utopia-php/database/zipball/8c764ed339caa60687b2362a96712bb6cbabf1eb", + "reference": "8c764ed339caa60687b2362a96712bb6cbabf1eb", "shasum": "" }, "require": { @@ -3540,9 +3540,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.71.6" + "source": "https://github.com/utopia-php/database/tree/feat-transaction-pinning" }, - "time": "2025-06-16T16:48:37+00:00" + "time": "2025-06-18T20:49:54+00:00" }, { "name": "utopia-php/detector", @@ -4807,16 +4807,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.41.7", + "version": "0.41.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "d8c7bb26ea32ab378faf4e0dfa62fd15fe37c57b" + "reference": "93ffb24b25b376ca4423e3a5caf6f916673af4b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d8c7bb26ea32ab378faf4e0dfa62fd15fe37c57b", - "reference": "d8c7bb26ea32ab378faf4e0dfa62fd15fe37c57b", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/93ffb24b25b376ca4423e3a5caf6f916673af4b2", + "reference": "93ffb24b25b376ca4423e3a5caf6f916673af4b2", "shasum": "" }, "require": { @@ -4852,9 +4852,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.41.7" + "source": "https://github.com/appwrite/sdk-generator/tree/0.41.8" }, - "time": "2025-06-13T17:05:57+00:00" + "time": "2025-06-18T13:20:45+00:00" }, { "name": "doctrine/annotations", @@ -8231,9 +8231,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-feat-transaction-pinning", + "alias": "0.71.6", + "alias_normalized": "0.71.6.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/database": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From 2745d8dce7edf4e614aee82d5250557ee44a5f61 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 18 Jun 2025 19:51:37 -0400 Subject: [PATCH 021/145] Add single op counting --- app/controllers/api/databases.php | 268 ++++++++++++++++++++++-------- 1 file changed, 195 insertions(+), 73 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 5b9d571211..26fe7a83b2 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1638,9 +1638,12 @@ App::post('/v1/databases/transactions/:transactionId/operations') $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { $dbForProject->createDocuments('transactionLogs', $staged); - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'operations' => $existing + \count($operations), - ])); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + \count($operations) + ); }); $response @@ -1775,7 +1778,14 @@ App::patch('/v1/databases/transactions/:transactionId') Query::equal('transactionInternalId', [$transaction->getSequence()]), ]); - $creates = $updates = $deletes = $bulkUpdates = $bulkDeletes = []; + $creates + = $updates + = $deletes + = $increments + = $decrements + = $bulkUpdates + = $bulkDeletes + = []; foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; @@ -1799,15 +1809,29 @@ App::patch('/v1/databases/transactions/:transactionId') case 'delete': $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; break; + case 'increment': + $increments[$databaseInternalId][$collectionInternalId][] = [ + 'attribute' => $operation['data']['attribute'], + 'value' => $operation['data']['value'] ?? 1, + 'max' => $operation['data']['max'] ?? null, + ]; + break; + case 'decrement': + $decrements[$databaseInternalId][$collectionInternalId][] = [ + 'attribute' => $operation['data']['attribute'], + 'value' => $operation['data']['value'] ?? 1, + 'min' => $operation['data']['min'] ?? null, + ]; + break; case 'bulkUpdate': $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ - 'data' => $operation['data'] ?? null, - 'queries' => $operation['queries'] ?? [], + 'data' => $operation['data']['data'] ?? null, + 'queries' => $operation['data']['queries'] ?? [], ]; break; case 'bulkDelete': $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ - 'queries' => $operation['queries'] ?? [], + 'queries' => $operation['data']['queries'] ?? [], ]; break; } @@ -1831,6 +1855,30 @@ App::patch('/v1/databases/transactions/:transactionId') ]); } } + foreach ($increments as $dbId => $cols) { + foreach ($cols as $colId => $increments) { + foreach ($increments as $increment) { + $dbForProject->increaseDocumentAttribute( + "database_{$dbId}_collection_{$colId}", + $increment['attribute'], + $increment['value'], + $increment['max'] + ); + } + } + } + foreach ($decrements as $dbId => $cols) { + foreach ($cols as $colId => $decrements) { + foreach ($decrements as $decrement) { + $dbForProject->decreaseDocumentAttribute( + "database_{$dbId}_collection_{$colId}", + $decrement['attribute'], + $decrement['value'], + $decrement['min'] + ); + } + } + } foreach ($bulkUpdates as $dbId => $cols) { foreach ($cols as $colId => $updates) { foreach ($updates as $update) { @@ -3654,7 +3702,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('user') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, ?string $documentId, string|array|null $data, ?array $permissions, ?array $documents, ?string $transactionId, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->inject('plan') + ->action(function (string $databaseId, string $collectionId, ?string $documentId, string|array|null $data, ?array $permissions, ?array $documents, ?string $transactionId, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan) { $data = \is_string($data) ? \json_decode($data, true) : $data; @@ -3903,7 +3952,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } try { - $dbForProject->createDocuments('transactionLogs', $operations); + $dbForProject->withTransaction(function () use ($dbForProject, $plan, $transactionId, $operations) { + $dbForProject->createDocuments('transactionLogs', $operations); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute:'operations', + value: \count($operations), + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (NotFoundException) { @@ -4442,7 +4500,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->inject('plan') + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data) && \is_null($permissions)) { @@ -4592,14 +4651,22 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $document->getId(), - 'action' => 'update', - 'data' => $newDocument->getArrayCopy(), - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $plan, $transactionId, $database, $collection, $transaction, $document, $newDocument) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $document->getId(), + 'action' => 'update', + 'data' => $newDocument->getArrayCopy(), + ])); + $dbForProject->increaseDocumentAttribute( + collection:'transactions', + id: $transactionId, + attribute: 'operations', + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); } else { try { $document = $dbForProject->updateDocument( @@ -4850,13 +4917,21 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'action' => 'update', - 'data' => $newDocument->getArrayCopy(), - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $newDocument) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'update', + 'data' => $newDocument->getArrayCopy(), + ])); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute: 'operations', + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); $upserted = $newDocument; } else { @@ -4991,18 +5066,25 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $documentId, - 'action' => 'increment', - 'data' => [ - 'attribute' => $attribute, - 'value' => $value, - 'max' => $max, - ] - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $documentId, $attribute, $value, $max) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'increment', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'max' => $max, + ] + ])); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + ); + }); $document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId); } else { @@ -5108,18 +5190,25 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $documentId, - 'action' => 'decrement', - 'data' => [ - 'attribute' => $attribute, - 'value' => $value, - 'min' => $min, - ], - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $documentId, $attribute, $value, $min) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'decrement', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'min' => $min, + ], + ])); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + ); + }); // Fetch current document for response without mutating $document = $dbForProject->getDocument( @@ -5249,13 +5338,21 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') $documents = []; if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'action' => 'bulkUpdate', - 'data' => \compact('data', 'queries'), - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $data, $queries) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'bulkUpdate', + 'data' => \compact('data', 'queries'), + ])); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute: 'operations', + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); } try { @@ -5368,10 +5465,19 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') ]); } - $dbForProject->createDocuments('transactionLogs', $operations); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $operations) { + $dbForProject->createDocuments('transactionLogs', $operations); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute: 'operations', + value: \count($operations), + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); - $modified = \count($documents); - $upserted = $documents; + $modified = \count($documents); + $upserted = $documents; } else { $upserted = []; @@ -5475,13 +5581,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu throw new Exception(Exception::TRANSACTION_INVALID, 'Transaction is not pending'); } - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $document->getId(), - 'action' => 'delete', - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $document) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $document->getId(), + 'action' => 'delete', + ])); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute: 'operations', + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); } else { try { $dbForProject->deleteDocument( @@ -5631,13 +5745,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') $documents = []; if (!empty($transactionId)) { - $dbForProject->createDocument('transactionLogs', new Document([ - 'databaseInternalId' => $database->getSequence(), - 'collectionInternalId' => $collection->getSequence(), - 'transactionInternalId' => $transaction->getSequence(), - 'action' => 'bulkDelete', - 'data' => ['queries' => $queries], - ])); + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $database, $collection, $transaction, $queries) { + $dbForProject->createDocument('transactionLogs', new Document([ + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'action' => 'bulkDelete', + 'data' => ['queries' => $queries], + ])); + $dbForProject->increaseDocumentAttribute( + collection: 'transactions', + id: $transactionId, + attribute: 'operations', + max: $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH, + ); + }); $modified = 0; } else { From 07cce5ad3c3198d7a41d941b5fadf2c46d8c8540 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 29 Jul 2025 21:30:02 +1200 Subject: [PATCH 022/145] Optional param --- app/controllers/api/databases.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 26fe7a83b2..9fa157c49d 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -5546,7 +5546,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', '', new UID(), 'Document ID.') - ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.') + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') From 480f8c97ca414113f7d2fa3d766edf4f794ee286 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 Aug 2025 08:25:58 +0000 Subject: [PATCH 023/145] Add transaction support for databases with staging and commit/rollback Co-authored-by: jakeb994 --- .../Documents/Attribute/Decrement.php | 3 +- .../Documents/Attribute/Increment.php | 3 +- .../Collections/Documents/Bulk/Delete.php | 3 +- .../Collections/Documents/Bulk/Update.php | 3 +- .../Collections/Documents/Bulk/Upsert.php | 3 +- .../Collections/Documents/Create.php | 3 +- .../Collections/Documents/Delete.php | 3 +- .../Collections/Documents/Update.php | 3 +- .../Collections/Documents/Upsert.php | 3 +- .../Http/Grids/Tables/Rows/Bulk/Delete.php | 1 + .../Http/Grids/Tables/Rows/Bulk/Update.php | 1 + .../Http/Grids/Tables/Rows/Bulk/Upsert.php | 1 + .../Grids/Tables/Rows/Column/Decrement.php | 1 + .../Grids/Tables/Rows/Column/Increment.php | 1 + .../Http/Grids/Tables/Rows/Create.php | 1 + .../Http/Grids/Tables/Rows/Delete.php | 1 + .../Http/Grids/Tables/Rows/Update.php | 1 + .../Http/Grids/Tables/Rows/Upsert.php | 1 + .../Http/Transactions/AddOperations.php | 117 ++++++++ .../Databases/Http/Transactions/Create.php | 73 +++++ .../Databases/Http/Transactions/Delete.php | 76 ++++++ .../Databases/Http/Transactions/Get.php | 69 +++++ .../Databases/Http/Transactions/Update.php | 249 ++++++++++++++++++ .../Databases/Http/Transactions/XList.php | 73 +++++ 24 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php 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 49a408e64c..fa6bc93845 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 @@ -74,6 +74,7 @@ class Decrement extends Action ->param('attribute', '', new Key(), 'Attribute key.') ->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true) ->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -81,7 +82,7 @@ class Decrement extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + 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): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { 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 5eadc96b9e..5bfbc96c7b 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 @@ -74,6 +74,7 @@ class Increment extends Action ->param('attribute', '', new Key(), 'Attribute key.') ->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true) ->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -81,7 +82,7 @@ class Increment extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { 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 f44e54f2b4..42206983bd 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 @@ -70,6 +70,7 @@ class Delete extends Action ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') @@ -81,7 +82,7 @@ class Delete extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { 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 82b39ef178..0a199c6eb7 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 @@ -74,6 +74,7 @@ class Update extends Action ->param('collectionId', '', new UID(), 'Collection ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true) ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') @@ -85,7 +86,7 @@ class Update extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $data = \is_string($data) ? \json_decode($data, true) 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 23e6453138..01844b52f4 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 @@ -71,6 +71,7 @@ class Upsert extends Action ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of document data as JSON objects. May contain partial documents.', false, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') @@ -78,7 +79,7 @@ class Upsert extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 0691249943..5d4629e7a5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -117,6 +117,7 @@ class Create extends Action ->param('data', [], new JSON(), 'Document data as JSON object.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('user') @@ -127,7 +128,7 @@ class Create extends Action ->inject('queueForWebhooks') ->callback($this->action(...)); } - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { $data = \is_string($data) ? \json_decode($data, true) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 7bc81ab7db..75377fe42b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -71,6 +71,7 @@ class Delete extends Action ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', '', new UID(), 'Document ID.') + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') @@ -79,7 +80,7 @@ class Delete extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); 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 1d06e6d0da..cda4563a4c 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 @@ -77,6 +77,7 @@ class Update extends Action ->param('documentId', '', new UID(), 'Document ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') @@ -85,7 +86,7 @@ class Update extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array 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 e862896a40..bdd5c1de0a 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 @@ -80,6 +80,7 @@ class Upsert extends Action ->param('documentId', '', new CustomId(), 'Document ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('user') @@ -89,7 +90,7 @@ class Upsert extends Action ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php index 6816fc3b13..e040757b7a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Delete.php @@ -56,6 +56,7 @@ class Delete extends DocumentsDelete ->param('databaseId', '', new UID(), 'Database ID.') ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tables#tablesCreate).') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php index 8c640f00f8..72381ee38f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Update.php @@ -58,6 +58,7 @@ class Update extends DocumentsUpdate ->param('tableId', '', new UID(), 'Table ID.') ->param('data', [], new JSON(), 'Row data as JSON object. Include only column and value pairs to be updated.', true) ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Upsert.php index 35f0a99bdc..230f81676a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Bulk/Upsert.php @@ -58,6 +58,7 @@ class Upsert extends DocumentsUpsert ->param('databaseId', '', new UID(), 'Database ID.') ->param('tableId', '', new UID(), 'Table ID.') ->param('rows', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of row data as JSON objects. May contain partial rows.', false, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php index 272510335f..cb04e118ca 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php @@ -60,6 +60,7 @@ class Decrement extends DecrementDocumentAttribute ->param('column', '', new Key(), 'Column key.') ->param('value', 1, new Numeric(), 'Value to increment the column by. The value must be a number.', true) ->param('min', null, new Numeric(), 'Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php index 2a28418a6e..4e36a2b912 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php @@ -60,6 +60,7 @@ class Increment extends IncrementDocumentAttribute ->param('column', '', new Key(), 'Column key.') ->param('value', 1, new Numeric(), 'Value to increment the column by. The value must be a number.', true) ->param('max', null, new Numeric(), 'Maximum value for the column. If the current value is greater than this value, an error will be thrown.', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php index d5a84bbb7c..dfbb9a61d2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php @@ -96,6 +96,7 @@ class Create extends DocumentCreate ->param('data', [], new JSON(), 'Row data as JSON object.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('rows', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan']) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') ->inject('user') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php index f821aabe6e..b13b17b8cd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php @@ -61,6 +61,7 @@ class Delete extends DocumentDelete ->param('databaseId', '', new UID(), 'Database ID.') ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tables#tablesCreate).') ->param('rowId', '', new UID(), 'Row ID.') + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php index 7731e10e9d..c6ce1a9795 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php @@ -60,6 +60,7 @@ class Update extends DocumentUpdate ->param('rowId', '', new UID(), 'Row ID.') ->param('data', [], new JSON(), 'Row data as JSON object. Include only columns and value pairs to be updated.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php index d9756eab87..157956ca18 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php @@ -62,6 +62,7 @@ class Upsert extends DocumentUpsert ->param('rowId', '', new UID(), 'Row ID.') ->param('data', [], new JSON(), 'Row data as JSON object. Include all required columns of the row to be created or updated.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('requestTimestamp') ->inject('response') ->inject('user') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php new file mode 100644 index 0000000000..197148a18a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php @@ -0,0 +1,117 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/transactions/:transactionId/operations') + ->desc('Add operations to transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'createOperations', + description: '/docs/references/databases/create-operations.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_CREATED, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('operations', [], new ArrayList(new Operation()), 'Array of staged operations.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('plan') + ->callback($this->action(...)); + } + + public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, array $plan): void + { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $existing = $transaction->getAttribute('operations', 0); + + if (($existing + \count($operations)) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding ' . \count($operations) . ' would exceed the maximum of ' . $maxBatch + ); + } + + $databases = $collections = $staged = []; + foreach ($operations as $operation) { + $database = $databases[$operation['databaseId']] ??= $dbForProject->getDocument('databases', $operation['databaseId']); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $collection = $collections[$operation['collectionId']] ??= $dbForProject->getDocument('database_' . $database->getSequence(), $operation['collectionId']); + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $staged[] = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $operation['documentId'] ?? ID::unique(), + 'action' => $operation['action'], + 'data' => $operation['data'] ?? new \stdClass(), + ]); + } + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + \count($operations) + ); + }); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php new file mode 100644 index 0000000000..9c4577c4c6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php @@ -0,0 +1,73 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/databases/transactions') + ->desc('Create transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'createTransaction', + description: '/docs/references/databases/create-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_CREATED, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true) + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } + + public function action(int $ttl, UtopiaResponse $response, Database $dbForProject): void + { + $transaction = $dbForProject->createDocument('transactions', new Document([ + '$id' => ID::unique(), + 'status' => 'pending', + 'operations' => 0, + 'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl), + ])); + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php new file mode 100644 index 0000000000..ae6f41fb2d --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php @@ -0,0 +1,76 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/databases/transactions/:transactionId') + ->desc('Delete transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'deleteTransaction', + description: '/docs/references/databases/delete-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_NOCONTENT, + model: UtopiaResponse::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } + + public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject): void + { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('transactions', $transactionId)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove transaction from DB'); + } + + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $response->noContent(); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php new file mode 100644 index 0000000000..1a8286e658 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php @@ -0,0 +1,69 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/transactions/:transactionId') + ->desc('Get transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'getTransaction', + description: '/docs/references/databases/get-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } + + public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject): void + { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php new file mode 100644 index 0000000000..d32e8d4c20 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -0,0 +1,249 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/databases/transactions/:transactionId') + ->desc('Update transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'collections.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'updateTransaction', + description: '/docs/references/databases/update-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('commit', false, new Boolean(), 'Commit transaction?', true) + ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) + ->inject('requestTimestamp') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->callback($this->action(...)); + } + + public function action(string $transactionId, bool $commit, bool $rollback, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Document $project): void + { + if (!$commit && !$rollback) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); + } + if ($commit && $rollback) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); + } + + $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); + } + + $now = new \DateTime(); + $expiresAt = new \DateTime($transaction->getAttribute('expiresAt', 'now')); + if ($now > $expiresAt) { + throw new Exception(Exception::TRANSACTION_EXPIRED); + } + + if ($commit) { + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committing', + ])); + + $operations = $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $creates + = $updates + = $deletes + = $increments + = $decrements + = $bulkUpdates + = $bulkDeletes + = []; + + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $documentId = $operation['documentId']; + + switch ($operation['action']) { + case 'create': + $creates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId ?? ID::unique(), + ...$operation['data'] + ]); + break; + case 'update': + case 'upsert': + $updates[$databaseInternalId][$collectionInternalId][] = new Document([ + '$id' => $documentId, + ...$operation['data'], + ]); + break; + case 'delete': + $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; + break; + case 'increment': + $increments[$databaseInternalId][$collectionInternalId][] = [ + 'attribute' => $operation['data']['attribute'], + 'value' => $operation['data']['value'] ?? 1, + 'max' => $operation['data']['max'] ?? null, + ]; + break; + case 'decrement': + $decrements[$databaseInternalId][$collectionInternalId][] = [ + 'attribute' => $operation['data']['attribute'], + 'value' => $operation['data']['value'] ?? 1, + 'min' => $operation['data']['min'] ?? null, + ]; + break; + case 'bulkUpdate': + $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ + 'data' => $operation['data']['data'] ?? null, + 'queries' => $operation['data']['queries'] ?? [], + ]; + break; + case 'bulkDelete': + $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ + 'queries' => $operation['data']['queries'] ?? [], + ]; + break; + } + } + + try { + foreach ($creates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createDocuments("database_{$dbId}_collection_{$colId}", $docs); + } + } + foreach ($updates as $dbId => $cols) { + foreach ($cols as $colId => $docs) { + $dbForProject->createOrUpdateDocuments("database_{$dbId}_collection_{$colId}", $docs); + } + } + foreach ($deletes as $dbId => $cols) { + foreach ($cols as $colId => $ids) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", [ + Query::equal('$id', $ids), + ]); + } + } + foreach ($increments as $dbId => $cols) { + foreach ($cols as $colId => $increments) { + foreach ($increments as $increment) { + $dbForProject->increaseDocumentAttribute( + "database_{$dbId}_collection_{$colId}", + $increment['attribute'], + $increment['value'], + $increment['max'] + ); + } + } + } + foreach ($decrements as $dbId => $cols) { + foreach ($cols as $colId => $decrements) { + foreach ($decrements as $decrement) { + $dbForProject->decreaseDocumentAttribute( + "database_{$dbId}_collection_{$colId}", + $decrement['attribute'], + $decrement['value'], + $decrement['min'] + ); + } + } + } + foreach ($bulkUpdates as $dbId => $cols) { + foreach ($cols as $colId => $updates) { + foreach ($updates as $update) { + $dbForProject->updateDocuments("database_{$dbId}_collection_{$colId}", $update['data'], $update['queries']); + } + } + } + foreach ($bulkDeletes as $dbId => $cols) { + foreach ($cols as $colId => $deletes) { + foreach ($deletes as $delete) { + $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", $delete['queries']); + } + } + } + + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committed', + ])); + + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + } catch (DuplicateException|ConflictException) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + + throw new Exception(Exception::TRANSACTION_CONFLICT); + } + }); + + $transaction = $dbForProject->getDocument('transactions', $transactionId); + } + + if ($rollback) { + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'rolledBack', + ])); + } + + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php new file mode 100644 index 0000000000..ee58807d6f --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php @@ -0,0 +1,73 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/databases/transactions') + ->desc('List transactions') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'databases', + group: 'transactions', + name: 'listTransactions', + description: '/docs/references/databases/list-transactions.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->param('queries', [], new Transactions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries).', true) + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } + + public function action(array $queries, UtopiaResponse $response, Database $dbForProject): void + { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $response->dynamic(new Document([ + 'transactions' => $dbForProject->find('transactions', $queries), + 'total' => $dbForProject->count('transactions', $queries), + ]), UtopiaResponse::MODEL_TRANSACTION_LIST); + } +} \ No newline at end of file From 5886c53ce3044916fb22b8ca7f74ea67e0cc6110 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 Aug 2025 08:43:30 +0000 Subject: [PATCH 024/145] Add transaction staging support for document operations Co-authored-by: jakeb994 --- .../Documents/Attribute/Decrement.php | 45 ++++++++++++++++ .../Documents/Attribute/Increment.php | 45 ++++++++++++++++ .../Collections/Documents/Bulk/Delete.php | 38 ++++++++++++++ .../Collections/Documents/Bulk/Update.php | 39 ++++++++++++++ .../Collections/Documents/Bulk/Upsert.php | 39 ++++++++++++++ .../Collections/Documents/Create.php | 51 +++++++++++++++++++ .../Collections/Documents/Delete.php | 33 ++++++++++++ .../Collections/Documents/Update.php | 42 +++++++++++++++ .../Collections/Documents/Upsert.php | 41 +++++++++++++++ 9 files changed, 373 insertions(+) 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 fa6bc93845..8742499348 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 @@ -94,6 +94,51 @@ class Decrement extends Action throw new Exception($this->getParentNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'decrement', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'min' => $min, + ], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually decrementing + $mockDocument = new Document([ + '$id' => $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + $attribute => $value, // Mock response - actual value would be computed during commit + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($mockDocument, $this->getResponseModel()); + return; + } + try { $document = $dbForProject->decreaseDocumentAttribute( collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), 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 5bfbc96c7b..f497d781f1 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 @@ -94,6 +94,51 @@ class Increment extends Action throw new Exception($this->getParentNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'increment', + 'data' => [ + 'attribute' => $attribute, + 'value' => $value, + 'max' => $max, + ], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually incrementing + $mockDocument = new Document([ + '$id' => $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + $attribute => $value, // Mock response - actual value would be computed during commit + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_OK) + ->dynamic($mockDocument, $this->getResponseModel()); + return; + } + try { $document = $dbForProject->increaseDocumentAttribute( collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), 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 42206983bd..19c9c7f005 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 @@ -109,6 +109,44 @@ class Delete extends Action throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => null, // Bulk operation doesn't have specific document ID + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => $queries, + ], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually deleting documents + $response->dynamic(new Document([ + $this->getSdkGroup() => [], + 'total' => 0, // Can't predict how many would be deleted + ]), $this->getResponseModel()); + return; + } + $documents = []; try { 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 0a199c6eb7..c2e2218c4e 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 @@ -121,6 +121,45 @@ class Update extends Action throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => null, // Bulk operation doesn't have specific document ID + 'action' => 'bulkUpdate', + 'data' => [ + 'data' => $data, + 'queries' => $queries, + ], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually updating documents + $response->dynamic(new Document([ + $this->getSdkGroup() => [], + 'total' => 0, // Can't predict how many would be updated + ]), $this->getResponseModel()); + return; + } + if ($data['$permissions']) { $validator = new Permissions(); if (!$validator->isValid($data['$permissions'])) { 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 01844b52f4..ba05ce414d 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 @@ -100,6 +100,45 @@ class Upsert extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // 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, + ]); + } + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + \count($staged) + ); + }); + + // Return successful response without actually upserting documents + $response->dynamic(new Document([ + $this->getSdkGroup() => [], + 'total' => \count($documents), + ]), $this->getResponseModel()); + return; + } + foreach ($documents as $key => $document) { $documents[$key] = new Document($document); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 5d4629e7a5..bf447454ed 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -189,6 +189,57 @@ class Create extends Action throw new Exception($this->getParentNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation(s) 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'] ?? $documentId ?? ID::unique(), + 'action' => 'create', + 'data' => $document, + ]); + } + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocuments('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + \count($staged) + ); + }); + + // Return successful response without actually creating documents + if ($isBulk) { + $response->dynamic(new Document([ + $this->getSdkGroup() => [], + 'total' => \count($documents), + ]), $this->getBulkResponseModel()); + } else { + $mockDocument = new Document([ + '$id' => $documents[0]['$id'] ?? $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + ...$documents[0] + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($mockDocument, $this->getResponseModel()); + } + return; + } + $hasRelationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 75377fe42b..e6dc7e1555 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -104,6 +104,39 @@ class Delete extends Action throw new Exception($this->getNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // Stage the operation in transaction logs + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $documentId, + 'action' => 'delete', + 'data' => [], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + 1 + ); + }); + + // Return successful response without actually deleting document + $response->noContent(); + return; + } + try { $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) { $dbForProject->deleteDocument( 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 cda4563a4c..33a1ebcc7a 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 @@ -128,6 +128,48 @@ class Update extends Action throw new Exception($this->getNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // 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, 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 bdd5c1de0a..61fdbd312d 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 @@ -111,6 +111,47 @@ class Upsert extends Action throw new Exception($this->getParentNotFoundException()); } + // Handle transaction staging + if ($transactionId !== null) { + $transaction = $dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + } + + // 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, From 5cec34b802ce310b7d24f113ebd7722eca5d09d4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 12 Aug 2025 00:45:30 +1200 Subject: [PATCH 025/145] Fix merge --- src/Appwrite/Utopia/Response.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 8cf5899823..1e8f9d5ee8 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -218,6 +218,10 @@ class Response extends SwooleResponse public const MODEL_COLUMN_DATETIME = 'columnDatetime'; public const MODEL_COLUMN_RELATIONSHIP = 'columnRelationship'; + // Transactions + public const MODEL_TRANSACTION = 'transaction'; + public const MODEL_TRANSACTION_LIST = 'transactionList'; + // Users public const MODEL_ACCOUNT = 'account'; public const MODEL_USER = 'user'; From a71333ac60ff164c482dc13fe0f7dfb2782e594f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 12 Aug 2025 12:00:28 +0000 Subject: [PATCH 026/145] Refactor transaction replay to preserve exact order and timestamps Co-authored-by: jakeb994 --- .../Databases/Http/Transactions/Update.php | 184 +++++++----------- 1 file changed, 72 insertions(+), 112 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index d32e8d4c20..8793847a38 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -93,124 +93,84 @@ class Update extends Action 'status' => 'committing', ])); + // Fetch operations ordered by creation time to maintain exact sequence $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc('$createdAt'), ]); - $creates - = $updates - = $deletes - = $increments - = $decrements - = $bulkUpdates - = $bulkDeletes - = []; - - foreach ($operations as $operation) { - $databaseInternalId = $operation['databaseInternalId']; - $collectionInternalId = $operation['collectionInternalId']; - $documentId = $operation['documentId']; - - switch ($operation['action']) { - case 'create': - $creates[$databaseInternalId][$collectionInternalId][] = new Document([ - '$id' => $documentId ?? ID::unique(), - ...$operation['data'] - ]); - break; - case 'update': - case 'upsert': - $updates[$databaseInternalId][$collectionInternalId][] = new Document([ - '$id' => $documentId, - ...$operation['data'], - ]); - break; - case 'delete': - $deletes[$databaseInternalId][$collectionInternalId][] = $documentId; - break; - case 'increment': - $increments[$databaseInternalId][$collectionInternalId][] = [ - 'attribute' => $operation['data']['attribute'], - 'value' => $operation['data']['value'] ?? 1, - 'max' => $operation['data']['max'] ?? null, - ]; - break; - case 'decrement': - $decrements[$databaseInternalId][$collectionInternalId][] = [ - 'attribute' => $operation['data']['attribute'], - 'value' => $operation['data']['value'] ?? 1, - 'min' => $operation['data']['min'] ?? null, - ]; - break; - case 'bulkUpdate': - $bulkUpdates[$databaseInternalId][$collectionInternalId][] = [ - 'data' => $operation['data']['data'] ?? null, - 'queries' => $operation['data']['queries'] ?? [], - ]; - break; - case 'bulkDelete': - $bulkDeletes[$databaseInternalId][$collectionInternalId][] = [ - 'queries' => $operation['data']['queries'] ?? [], - ]; - break; - } - } - try { - foreach ($creates as $dbId => $cols) { - foreach ($cols as $colId => $docs) { - $dbForProject->createDocuments("database_{$dbId}_collection_{$colId}", $docs); - } - } - foreach ($updates as $dbId => $cols) { - foreach ($cols as $colId => $docs) { - $dbForProject->createOrUpdateDocuments("database_{$dbId}_collection_{$colId}", $docs); - } - } - foreach ($deletes as $dbId => $cols) { - foreach ($cols as $colId => $ids) { - $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", [ - Query::equal('$id', $ids), - ]); - } - } - foreach ($increments as $dbId => $cols) { - foreach ($cols as $colId => $increments) { - foreach ($increments as $increment) { - $dbForProject->increaseDocumentAttribute( - "database_{$dbId}_collection_{$colId}", - $increment['attribute'], - $increment['value'], - $increment['max'] - ); + // Replay operations in exact order they were created + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $documentId = $operation['documentId']; + $action = $operation['action']; + $data = $operation['data']; + $operationCreatedAt = new \DateTime($operation['$createdAt']); + + $collectionName = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; + + // Wrap each operation with the timestamp from when it was logged + $dbForProject->withRequestTimestamp($operationCreatedAt, function () use ($dbForProject, $action, $collectionName, $documentId, $data) { + switch ($action) { + case 'create': + $document = new Document([ + '$id' => $documentId ?? ID::unique(), + ...$data + ]); + $dbForProject->createDocument($collectionName, $document); + break; + + case 'update': + case 'upsert': + $document = new Document([ + '$id' => $documentId, + ...$data, + ]); + $dbForProject->createOrUpdateDocument($collectionName, $document); + break; + + case 'delete': + $dbForProject->deleteDocument($collectionName, $documentId); + break; + + case 'increment': + $dbForProject->increaseDocumentAttribute( + collection: $collectionName, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + max: $data['max'] ?? null + ); + break; + + case 'decrement': + $dbForProject->decreaseDocumentAttribute( + collection: $collectionName, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + min: $data['min'] ?? null + ); + break; + + case 'bulkUpdate': + $dbForProject->updateDocuments( + $collectionName, + $data['data'] ?? null, + $data['queries'] ?? [] + ); + break; + + case 'bulkDelete': + $dbForProject->deleteDocuments( + $collectionName, + $data['queries'] ?? [] + ); + break; } - } - } - foreach ($decrements as $dbId => $cols) { - foreach ($cols as $colId => $decrements) { - foreach ($decrements as $decrement) { - $dbForProject->decreaseDocumentAttribute( - "database_{$dbId}_collection_{$colId}", - $decrement['attribute'], - $decrement['value'], - $decrement['min'] - ); - } - } - } - foreach ($bulkUpdates as $dbId => $cols) { - foreach ($cols as $colId => $updates) { - foreach ($updates as $update) { - $dbForProject->updateDocuments("database_{$dbId}_collection_{$colId}", $update['data'], $update['queries']); - } - } - } - foreach ($bulkDeletes as $dbId => $cols) { - foreach ($cols as $colId => $deletes) { - foreach ($deletes as $delete) { - $dbForProject->deleteDocuments("database_{$dbId}_collection_{$colId}", $delete['queries']); - } - } + }); } $dbForProject->updateDocument('transactions', $transactionId, new Document([ From 6d477688d74a53b163b090066fd6e7ba03739bf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 14 Aug 2025 09:50:45 +0000 Subject: [PATCH 027/145] Enforce max operations per transaction in database operations Co-authored-by: jakeb994 --- .../Collections/Documents/Attribute/Decrement.php | 15 ++++++++++++++- .../Collections/Documents/Attribute/Increment.php | 15 ++++++++++++++- .../Collections/Documents/Bulk/Delete.php | 10 ++++++++++ .../Collections/Documents/Bulk/Update.php | 10 ++++++++++ .../Collections/Documents/Bulk/Upsert.php | 10 ++++++++++ .../Databases/Collections/Documents/Create.php | 13 ++++++++++++- .../Databases/Collections/Documents/Delete.php | 15 ++++++++++++++- .../Databases/Collections/Documents/Update.php | 15 ++++++++++++++- .../Databases/Collections/Documents/Upsert.php | 13 ++++++++++++- 9 files changed, 110 insertions(+), 6 deletions(-) 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 8742499348..b383bffbe6 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 @@ -79,10 +79,13 @@ class Decrement extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->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): void + 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, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { @@ -101,6 +104,16 @@ class Decrement extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 f497d781f1..311c07dedf 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 @@ -79,10 +79,13 @@ class Increment extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { @@ -101,6 +104,16 @@ class Increment extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 19c9c7f005..b52f38cf7b 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 @@ -116,6 +116,16 @@ class Delete extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 c2e2218c4e..c6ea163604 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 @@ -128,6 +128,16 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 ba05ce414d..50ca33d002 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 @@ -107,6 +107,16 @@ class Upsert extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $existing = $transaction->getAttribute('operations', 0); + if (($existing + \count($documents)) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch + ); + } + // Stage the operations in transaction logs $staged = []; foreach ($documents as $document) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index bf447454ed..3a544bb869 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -126,9 +126,10 @@ class Create extends Action ->inject('queueForRealtime') ->inject('queueForFunctions') ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $data = \is_string($data) ? \json_decode($data, true) @@ -196,6 +197,16 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $existing = $transaction->getAttribute('operations', 0); + if (($existing + \count($documents)) > $maxBatch) { + throw new Exception( + Exception::TRANSACTION_LIMIT_EXCEEDED, + 'Transaction already has ' . $existing . ' operations, adding ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch + ); + } + // Stage the operation(s) in transaction logs $staged = []; foreach ($documents as $document) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index e6dc7e1555..c6a2218f54 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -77,10 +77,13 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -111,6 +114,16 @@ class Delete extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 33a1ebcc7a..a616f592cb 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 @@ -83,10 +83,13 @@ class Update extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -135,6 +138,16 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), 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 61fdbd312d..b69f58f48b 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 @@ -87,10 +87,11 @@ class Upsert extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -118,6 +119,16 @@ class Upsert extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } + // Enforce max operations per transaction + $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $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(), From dffe030d16ee4ae53297329eb339abf384d86656 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 14 Aug 2025 10:06:43 +0000 Subject: [PATCH 028/145] Replace databasesBatchSize with databasesTransactionSize in database operations Co-authored-by: jakeb994 --- app/init/constants.php | 1 + .../Databases/Collections/Documents/Attribute/Decrement.php | 2 +- .../Databases/Collections/Documents/Attribute/Increment.php | 2 +- .../Http/Databases/Collections/Documents/Bulk/Delete.php | 2 +- .../Http/Databases/Collections/Documents/Bulk/Update.php | 2 +- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 2 +- .../Databases/Http/Databases/Collections/Documents/Create.php | 2 +- .../Databases/Http/Databases/Collections/Documents/Delete.php | 2 +- .../Databases/Http/Databases/Collections/Documents/Update.php | 2 +- .../Databases/Http/Databases/Collections/Documents/Upsert.php | 2 +- .../Modules/Databases/Http/Transactions/AddOperations.php | 2 +- 11 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index d217174215..c5b83f2594 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -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 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 b383bffbe6..7013e4bce6 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 @@ -105,7 +105,7 @@ class Decrement extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 311c07dedf..24a521724b 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 @@ -105,7 +105,7 @@ class Increment extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 b52f38cf7b..0c27ea420a 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 @@ -117,7 +117,7 @@ class Delete extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 c6ea163604..8cca45661d 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 @@ -129,7 +129,7 @@ class Update extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 50ca33d002..0868a2519b 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 @@ -108,7 +108,7 @@ class Upsert extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + \count($documents)) > $maxBatch) { throw new Exception( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 3a544bb869..e0d7d03180 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -198,7 +198,7 @@ class Create extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + \count($documents)) > $maxBatch) { throw new Exception( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index c6a2218f54..59f72d727d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -115,7 +115,7 @@ class Delete extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 a616f592cb..76d3bc692b 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 @@ -139,7 +139,7 @@ class Update extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( 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 b69f58f48b..fff6c53978 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 @@ -120,7 +120,7 @@ class Upsert extends Action } // Enforce max operations per transaction - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + 1) > $maxBatch) { throw new Exception( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php index 197148a18a..d8d31f0d8f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php @@ -67,7 +67,7 @@ class AddOperations extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } - $maxBatch = $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH; + $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION; $existing = $transaction->getAttribute('operations', 0); if (($existing + \count($operations)) > $maxBatch) { From 335052bc847930012482f6e8ebfb7ad3ffebb74d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 14 Aug 2025 10:17:54 +0000 Subject: [PATCH 029/145] Remove unused function and webhook event queue injections in database actions Co-authored-by: jakeb994 --- .../Databases/Collections/Documents/Attribute/Decrement.php | 4 +--- .../Databases/Collections/Documents/Attribute/Increment.php | 4 +--- .../Databases/Http/Databases/Collections/Documents/Create.php | 4 +--- .../Databases/Http/Databases/Collections/Documents/Delete.php | 4 +--- .../Databases/Http/Databases/Collections/Documents/Update.php | 4 +--- 5 files changed, 5 insertions(+), 15 deletions(-) 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 7013e4bce6..f71ed4f187 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 @@ -79,13 +79,11 @@ class Decrement extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('queueForFunctions') - ->inject('queueForWebhooks') ->inject('plan') ->callback($this->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, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + 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)); if ($database->isEmpty()) { 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 24a521724b..d9d9beefa7 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 @@ -79,13 +79,11 @@ class Increment extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('queueForFunctions') - ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index e0d7d03180..fe8b688a54 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -124,12 +124,10 @@ class Create extends Action ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('queueForRealtime') - ->inject('queueForFunctions') - ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, array $plan): void { $data = \is_string($data) ? \json_decode($data, true) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 59f72d727d..b176d24ef5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -77,13 +77,11 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('queueForFunctions') - ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); 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 76d3bc692b..611652b48d 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 @@ -83,13 +83,11 @@ class Update extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('queueForFunctions') - ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array From 31a7df721488e704be720092fafc04faf8dab215 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 14 Aug 2025 22:20:30 +1200 Subject: [PATCH 030/145] Import fixes --- .../Http/Databases/Collections/Documents/Bulk/Delete.php | 1 + .../Http/Databases/Collections/Documents/Bulk/Update.php | 3 ++- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 1 + .../Http/Databases/Collections/Documents/Create.php | 7 +++++-- .../Http/Databases/Collections/Documents/Delete.php | 9 +++++++-- .../Http/Databases/Collections/Documents/Update.php | 8 ++++++-- .../Http/Databases/Collections/Documents/Upsert.php | 7 +++++-- .../Modules/Databases/Http/Transactions/Create.php | 3 +-- .../{AddOperations.php => Operations/Create.php} | 4 ++-- 9 files changed, 30 insertions(+), 13 deletions(-) rename src/Appwrite/Platform/Modules/Databases/Http/Transactions/{AddOperations.php => Operations/Create.php} (97%) 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 19c9c7f005..727daa8576 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 @@ -17,6 +17,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Restricted as RestrictedException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; 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 c2e2218c4e..d34c819fc4 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 @@ -18,6 +18,7 @@ use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Structure as StructureException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; @@ -108,7 +109,7 @@ class Update extends Action $hasRelationships = \array_filter( $collection->getAttribute('attributes', []), - fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP ); if ($hasRelationships) { 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 ba05ce414d..ca94c19302 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 @@ -17,6 +17,7 @@ use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Structure as StructureException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\ArrayList; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 1090b869f5..2eb1896b51 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -194,8 +194,11 @@ class Create extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); } // Stage the operation(s) in transaction logs diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index e6dc7e1555..925b046d17 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -13,8 +13,10 @@ 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\Restricted as RestrictedException; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -107,8 +109,11 @@ class Delete extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); } // Stage the operation in transaction logs 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 33a1ebcc7a..ef65fe9d40 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 @@ -120,6 +120,7 @@ class Update extends Action throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); } } + // Read permission should not be required for update /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); @@ -131,8 +132,11 @@ class Update extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); } // Stage the operation in transaction logs 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 61fdbd312d..e598a69be7 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 @@ -114,8 +114,11 @@ class Upsert extends Action // Handle transaction staging if ($transactionId !== null) { $transaction = $dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); + if ($transaction->isEmpty()) { + throw new Exception(Exception::TRANSACTION_NOT_FOUND); + } + if ($transaction->getAttribute('status', '') !== 'pending') { + throw new Exception(Exception::TRANSACTION_NOT_READY); } // Stage the operation in transaction logs diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php index 9c4577c4c6..25a16e19e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php @@ -9,10 +9,9 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; -use Utopia\Database\Validator\UID; -use Utopia\DateTime\DateTime; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\Range; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php similarity index 97% rename from src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php rename to src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php index 197148a18a..40f03e4ccb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/AddOperations.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php @@ -1,6 +1,6 @@ Date: Thu, 14 Aug 2025 22:22:12 +1200 Subject: [PATCH 031/145] Fix create inject --- .../Databases/Http/Databases/Collections/Documents/Create.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 0327677372..8fd334fd1e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -126,10 +126,12 @@ class Create extends Action ->inject('queueForEvents') ->inject('queueForStatsUsage') ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, array $plan): void + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan): void { $data = \is_string($data) ? \json_decode($data, true) From 76034f801b5893a6a756d0ccbaaee7edce3d47a0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:31:27 +1200 Subject: [PATCH 032/145] Add failed error --- app/config/errors.php | 7 ++++++- src/Appwrite/Extend/Exception.php | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/config/errors.php b/app/config/errors.php index fe64a0ce05..d786940c07 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -963,7 +963,12 @@ return [ ], Exception::TRANSACTION_INVALID => [ 'name' => Exception::TRANSACTION_INVALID, - 'description' => 'The transaction is invalid. Please check the transaction data and try again.', + '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 => [ diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 73185d8fab..2465623d18 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -267,6 +267,7 @@ class Exception extends \Exception public const TRANSACTION_NOT_FOUND = 'transaction_not_found'; public const TRANSACTION_ALREADY_EXISTS = 'transaction_already_exists'; public const TRANSACTION_INVALID = 'transaction_invalid'; + public const TRANSACTION_FAILED = 'transaction_expired'; public const TRANSACTION_EXPIRED = 'transaction_expired'; public const TRANSACTION_CONFLICT = 'transaction_conflict'; public const TRANSACTION_LIMIT_EXCEEDED = 'transaction_limit_exceeded'; From 22358482627f7ade32392ba567283386ec61b2c8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:32:03 +1200 Subject: [PATCH 033/145] Clear txn logs on worker --- app/init/constants.php | 2 ++ .../Databases/Http/Transactions/Update.php | 13 +++++++------ src/Appwrite/Platform/Workers/Deletes.php | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index c5b83f2594..e811fbe306 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -105,8 +105,10 @@ 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_PROJECTS = 'projects'; const DELETE_TYPE_SITES = 'sites'; const DELETE_TYPE_FUNCTIONS = 'functions'; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 8793847a38..db17157783 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -177,9 +177,10 @@ class Update extends Action 'status' => 'committed', ])); - $dbForProject->deleteDocuments('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - ]); + // Clear the transaction logs + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); } catch (DuplicateException|ConflictException) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', @@ -193,9 +194,9 @@ class Update extends Action } if ($rollback) { - $dbForProject->deleteDocuments('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - ]); + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'rolledBack', diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index aa511c2209..c7dda8ccf0 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -130,6 +130,9 @@ class Deletes extends Action case DELETE_TYPE_RULES: $this->deleteRule($dbForPlatform, $document, $certificates); break; + case DELETE_TYPE_TRANSACTION: + $this->deleteTransactionLogs($getProjectDB, $document, $project); + break; default: Console::error('No lazy delete operation available for document of type: ' . $document->getCollection()); break; @@ -1299,4 +1302,20 @@ class Deletes extends Action ); } } + + private function deleteTransactionLogs(callable $getProjectDB, Document $document, Document $project): void + { + $dbForProject = $getProjectDB($project); + $transactionId = $document->getId(); + $transactionInternalId = $document->getSequence(); + + try { + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', [$transactionInternalId]), + ]); + Console::info("Transaction logs for {$transactionId} deleted."); + } catch (Throwable $th) { + Console::error("Failed to delete transaction logs for {$transactionId}: " . $th->getMessage()); + } + } } From 3d1ae4feda6d0ab38a050cc2c179305de9bc717b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:34:20 +1200 Subject: [PATCH 034/145] Process permissions before txn logging --- .../Documents/Attribute/Decrement.php | 8 +- .../Documents/Attribute/Increment.php | 4 +- .../Collections/Documents/Bulk/Delete.php | 2 - .../Collections/Documents/Bulk/Update.php | 15 ++- .../Collections/Documents/Bulk/Upsert.php | 33 +++--- .../Collections/Documents/Update.php | 112 +++++++++--------- .../Collections/Documents/Upsert.php | 108 ++++++++--------- 7 files changed, 141 insertions(+), 141 deletions(-) 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) { From ede88a533ec31cf6ef374ee54f79e6af03ee6ae1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:35:06 +1200 Subject: [PATCH 035/145] Handle create single/bulk split --- .../Collections/Documents/Create.php | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 8fd334fd1e..5e7c20b76d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -192,70 +192,6 @@ class Create 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 + \count($documents)) > $maxBatch) { - throw new Exception( - Exception::TRANSACTION_LIMIT_EXCEEDED, - 'Transaction already has ' . $existing . ' operations, adding ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch - ); - } - - // Stage the operation(s) 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'] ?? $documentId ?? ID::unique(), - 'action' => 'create', - 'data' => $document, - ]); - } - - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { - $dbForProject->createDocuments('transactionLogs', $staged); - $dbForProject->increaseDocumentAttribute( - 'transactions', - $transactionId, - 'operations', - \count($staged) - ); - }); - - // Return successful response without actually creating documents - if ($isBulk) { - $response->dynamic(new Document([ - $this->getSdkGroup() => [], - 'total' => \count($documents), - ]), $this->getBulkResponseModel()); - } else { - $mockDocument = new Document([ - '$id' => $documents[0]['$id'] ?? $documentId, - '$collectionId' => $collectionId, - '$databaseId' => $databaseId, - ...$documents[0] - ]); - $response - ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) - ->dynamic($mockDocument, $this->getResponseModel()); - } - return; - } - $hasRelationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP @@ -439,6 +375,65 @@ class Create extends Action return $document; }, $documents); + // 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 ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch + ); + } + + $staged = new Document([ + '$id' => ID::unique(), + 'databaseInternalId' => $database->getSequence(), + 'collectionInternalId' => $collection->getSequence(), + 'transactionInternalId' => $transaction->getSequence(), + 'documentId' => $isBulk ? null: $documentId, + 'action' => $isBulk ? 'bulkCreate' : 'create', + 'data' => $isBulk ? $documents : $documents[0], + ]); + + $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) { + $dbForProject->createDocument('transactionLogs', $staged); + $dbForProject->increaseDocumentAttribute( + 'transactions', + $transactionId, + 'operations', + ); + }); + + // Return successful response without actually creating documents + if ($isBulk) { + $response->dynamic(new Document([ + $this->getSdkGroup() => [], + 'total' => \count($documents), + ]), $this->getBulkResponseModel()); + } else { + $mockDocument = new Document([ + '$id' => $documents[0]['$id'] ?? $documentId, + '$collectionId' => $collectionId, + '$databaseId' => $databaseId, + ...$documents[0] + ]); + $response + ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) + ->dynamic($mockDocument, $this->getResponseModel()); + } + return; + } + try { $dbForProject->withPreserveDates( fn () => $dbForProject->createDocuments( From 84bf2641aae9d813f5fdbe425a2c4ff31858da63 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:35:20 +1200 Subject: [PATCH 036/145] Fix scope --- .../Platform/Modules/Databases/Http/Transactions/Update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index db17157783..d41cdbf20d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -38,7 +38,7 @@ class Update extends Action ->setHttpPath('/v1/databases/transactions/:transactionId') ->desc('Update transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'collections.write') + ->label('scope', 'transactions.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', From 3a7d8d2296fdfdf0ef065fc29be436820fc99138 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:35:41 +1200 Subject: [PATCH 037/145] Remove redundant order --- .../Modules/Databases/Http/Transactions/Update.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index d41cdbf20d..d7da3c0df5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -57,14 +57,13 @@ class Update extends Action ->param('transactionId', '', new UID(), 'Transaction ID.') ->param('commit', false, new Boolean(), 'Commit transaction?', true) ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) - ->inject('requestTimestamp') ->inject('response') ->inject('dbForProject') - ->inject('project') + ->inject('queueForDeletes') ->callback($this->action(...)); } - public function action(string $transactionId, bool $commit, bool $rollback, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Document $project): void + public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Delete $queueForDeletes): void { if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); @@ -88,15 +87,14 @@ class Update extends Action } if ($commit) { - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $transaction, $requestTimestamp) { + $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, $transaction) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', ])); - // Fetch operations ordered by creation time to maintain exact sequence + // Fetch operations ordered by sequence by default $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc('$createdAt'), ]); try { From d6544f412de4742e341afdbbe4e5cb0d0be63a02 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:36:26 +1200 Subject: [PATCH 038/145] Add missing bulk cases on commit --- .../Databases/Http/Transactions/Update.php | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index d7da3c0df5..0240f3e9f6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -152,14 +152,30 @@ class Update extends Action min: $data['min'] ?? null ); break; - + + case 'bulkCreate': + $documents = []; + foreach ($data as $docData) { + $documents[] = new Document($docData); + } + $dbForProject->createDocuments($collectionId, $documents); + break; + case 'bulkUpdate': $dbForProject->updateDocuments( - $collectionName, + $collectionId, $data['data'] ?? null, $data['queries'] ?? [] ); break; + + case 'bulkUpsert': + $documents = []; + foreach ($data as $docData) { + $documents[] = new Document($docData); + } + $dbForProject->createOrUpdateDocuments($collectionId, $documents); + break; case 'bulkDelete': $dbForProject->deleteDocuments( From fb31fbea9bf06a593b60a3e211c9e24187f1cc53 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:36:42 +1200 Subject: [PATCH 039/145] Ensure parsed queries --- .../Platform/Modules/Databases/Http/Transactions/Update.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 0240f3e9f6..419d83eb41 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -165,7 +165,7 @@ class Update extends Action $dbForProject->updateDocuments( $collectionId, $data['data'] ?? null, - $data['queries'] ?? [] + Query::parseQueries($data['queries'] ?? []) ); break; @@ -179,8 +179,8 @@ class Update extends Action case 'bulkDelete': $dbForProject->deleteDocuments( - $collectionName, - $data['queries'] ?? [] + $collectionId, + Query::parseQueries($data['queries'] ?? []) ); break; } From 5cd99de6e4ec969291de7f42b084f13d57e7110c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:36:59 +1200 Subject: [PATCH 040/145] Remove redundant read --- .../Platform/Modules/Databases/Http/Transactions/Update.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 419d83eb41..ee40bbc6cd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -187,7 +187,7 @@ class Update extends Action }); } - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committed', ])); @@ -203,8 +203,6 @@ class Update extends Action throw new Exception(Exception::TRANSACTION_CONFLICT); } }); - - $transaction = $dbForProject->getDocument('transactions', $transactionId); } if ($rollback) { From 9838571184f437b80a08d53b49419fef7c2c0a7a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:37:18 +1200 Subject: [PATCH 041/145] Catch explicit txn exception --- .../Platform/Modules/Databases/Http/Transactions/Update.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index ee40bbc6cd..d42e242e82 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -201,6 +201,12 @@ class Update extends Action ])); throw new Exception(Exception::TRANSACTION_CONFLICT); + } catch (TransactionException $e) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + + throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } }); } From dfeadfd50f24885b3336e30c3ee182d68f8851d1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:37:29 +1200 Subject: [PATCH 042/145] Update naming --- .../Databases/Http/Transactions/Update.php | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index d42e242e82..27251f3fd9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Transactions; +use Appwrite\Event\Delete; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; @@ -13,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; @@ -102,40 +104,37 @@ class Update extends Action foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; $collectionInternalId = $operation['collectionInternalId']; + $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; $documentId = $operation['documentId']; + $createdAt = new \DateTime($operation['$createdAt']); $action = $operation['action']; $data = $operation['data']; - $operationCreatedAt = new \DateTime($operation['$createdAt']); - - $collectionName = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; // Wrap each operation with the timestamp from when it was logged - $dbForProject->withRequestTimestamp($operationCreatedAt, function () use ($dbForProject, $action, $collectionName, $documentId, $data) { + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $queueForDeletes, $action, $collectionId, $documentId, $data) { switch ($action) { case 'create': - $document = new Document([ - '$id' => $documentId ?? ID::unique(), - ...$data - ]); - $dbForProject->createDocument($collectionName, $document); + $document = new Document($data); + $dbForProject->createDocument($collectionId, $document); break; - + case 'update': + $document = new Document($data); + $dbForProject->updateDocument($collectionId, $documentId, $document); + break; + case 'upsert': - $document = new Document([ - '$id' => $documentId, - ...$data, - ]); - $dbForProject->createOrUpdateDocument($collectionName, $document); + $document = new Document($data); + $dbForProject->createOrUpdateDocuments($collectionId, [$document]); break; case 'delete': - $dbForProject->deleteDocument($collectionName, $documentId); + $dbForProject->deleteDocument($collectionId, $documentId); break; case 'increment': $dbForProject->increaseDocumentAttribute( - collection: $collectionName, + collection: $collectionId, id: $documentId, attribute: $data['attribute'], value: $data['value'] ?? 1, @@ -145,7 +144,7 @@ class Update extends Action case 'decrement': $dbForProject->decreaseDocumentAttribute( - collection: $collectionName, + collection: $collectionId, id: $documentId, attribute: $data['attribute'], value: $data['value'] ?? 1, From 6e6364638dcef2b5e28f2ca31e31b9efec13e374 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:37:39 +1200 Subject: [PATCH 043/145] Add missing validator ops --- src/Appwrite/Utopia/Database/Validator/Operation.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 989fd76eec..17a2839f59 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -21,7 +21,9 @@ class Operation extends Validator 'update', 'upsert', 'delete', + 'bulkCreate', 'bulkUpdate', + 'bulkUpsert', 'bulkDelete', ]; From 2a5ab1f8aae86eae30f30c94917e46cb49e2f870 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:48:37 +1200 Subject: [PATCH 044/145] Update src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Utopia/Database/Validator/Queries/Transactions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php index ab3e933d6f..8a9604a7ae 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php @@ -4,7 +4,8 @@ namespace Appwrite\Utopia\Database\Validator\Queries; class Transactions extends Base { - public const array ALLOWED_ATTRIBUTES = [ + /** @var string[] */ + public const ALLOWED_ATTRIBUTES = [ 'status', 'expiresAt', ]; From 875338d133273c04acb111e615011782a6e32ca2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 00:48:56 +1200 Subject: [PATCH 045/145] Update src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php index 8a9604a7ae..2f557e5489 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php @@ -12,6 +12,6 @@ class Transactions extends Base public function __construct() { - parent::__construct('functions', self::ALLOWED_ATTRIBUTES); + parent::__construct('transactions', self::ALLOWED_ATTRIBUTES); } } From e9c730e0c1ed267d90a70a3d694e6502158cfddd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:03:18 +1200 Subject: [PATCH 046/145] Add txn id to create multi-method params --- .../Databases/Http/Databases/Collections/Documents/Create.php | 2 ++ .../Utopia/Database/Validator/Queries/Transactions.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 5e7c20b76d..6c9a7ee228 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -82,6 +82,7 @@ class Create extends Action new Parameter('documentId', optional: false), new Parameter('data', optional: false), new Parameter('permissions', optional: true), + new Parameter('transactionId', optional: true), ], deprecated: new Deprecated( since: '1.8.0', @@ -106,6 +107,7 @@ class Create extends Action new Parameter('databaseId', optional: false), new Parameter('collectionId', optional: false), new Parameter('documents', optional: false), + new Parameter('transactionId', optional: true), ], deprecated: new Deprecated( since: '1.8.0', diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php index 2f557e5489..b49494c0af 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Transactions.php @@ -4,8 +4,8 @@ namespace Appwrite\Utopia\Database\Validator\Queries; class Transactions extends Base { - /** @var string[] */ - public const ALLOWED_ATTRIBUTES = [ + /** @var array */ + public const array ALLOWED_ATTRIBUTES = [ 'status', 'expiresAt', ]; From 56d7716ddb1998ea01a3300f59e7d6a755152d86 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:03:36 +1200 Subject: [PATCH 047/145] Require data param for ops --- src/Appwrite/Utopia/Database/Validator/Operation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 17a2839f59..8ef3817668 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -13,6 +13,7 @@ class Operation extends Validator 'databaseId', 'collectionId', 'action', + 'data', ]; /** @var array */ From 0533568f9e6ad385c123acac5839b9ec6937183b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:03:46 +1200 Subject: [PATCH 048/145] Fix test --- tests/e2e/Services/Databases/Legacy/DatabasesBase.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 11d64077d9..ddc0af99c7 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -6195,10 +6195,10 @@ trait DatabasesBase $this->assertEquals(200, $rollback['headers']['status-code']); $this->assertEquals('rolledBack', $rollback['body']['status']); - $documents = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents", [ + $documents = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents", \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()); + ], $this->getHeaders())); $this->assertEquals(0, count($documents['body']['documents'])); } @@ -6260,7 +6260,7 @@ trait DatabasesBase 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'create', - 'data' => ['attribute' => 'value'], + 'data' => ['name' => 'value'], ] ] ]); From bed46be72005bf6b34b44c3841e10e9973c904e8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:05:03 +1200 Subject: [PATCH 049/145] Add grids params --- .../Modules/Databases/Http/Grids/Tables/Rows/Create.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php index db6029def9..dbebfd28c4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php @@ -69,6 +69,7 @@ class Create extends DocumentCreate new Parameter('rowId', optional: false), new Parameter('data', optional: false), new Parameter('permissions', optional: true), + new Parameter('transactionId', optional: true), ] ), new Method( @@ -89,6 +90,7 @@ class Create extends DocumentCreate new Parameter('databaseId', optional: false), new Parameter('tableId', optional: false), new Parameter('rows', optional: false), + new Parameter('transactionId', optional: true), ] ) ]) @@ -97,7 +99,7 @@ class Create extends DocumentCreate ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tables#tablesCreate). Make sure to define columns before creating rows.') ->param('data', [], new JSON(), 'Row data as JSON object.', true) ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) - ->param('rows', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan']) + ->param('rows', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of rows data as JSON objects.', true, ['plan']) ->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true) ->inject('response') ->inject('dbForProject') From ad8cebc6e3daf9f8cd8045e2a7570ec62d9c2a4f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:32:09 +1200 Subject: [PATCH 050/145] Ensure updated txn is returned from bulk op add --- .../Modules/Databases/Http/Transactions/Operations/Create.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php index 4974556cec..91631fc257 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php @@ -100,9 +100,9 @@ class Create extends Action ]); } - $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $transaction = $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { $dbForProject->createDocuments('transactionLogs', $staged); - $dbForProject->increaseDocumentAttribute( + return $dbForProject->increaseDocumentAttribute( 'transactions', $transactionId, 'operations', From 46f8d76d7a47fd2284a14da0335e358d442d6000 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:32:21 +1200 Subject: [PATCH 051/145] Fix defualts for bulk op add --- .../Modules/Databases/Http/Transactions/Operations/Create.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php index 91631fc257..74f6337751 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php @@ -94,9 +94,9 @@ class Create extends Action 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $operation['documentId'] ?? ID::unique(), + 'documentId' => $operation['documentId'] ?? null, 'action' => $operation['action'], - 'data' => $operation['data'] ?? new \stdClass(), + 'data' => $operation['data'] ?? [], ]); } From 2b3795224d707756059b3c58eb3abfcc3d5c1e90 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:33:22 +1200 Subject: [PATCH 052/145] Check for documentId on ops that require it --- src/Appwrite/Utopia/Database/Validator/Operation.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 8ef3817668..ac585e26bc 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -71,6 +71,15 @@ class Operation extends Validator return false; } + // If action requires documentId, it must be present + if ( + isset($this->requiresDocumentId[$value['action']]) && + !\array_key_exists('documentId', $value) + ) { + $this->description = "Key 'documentId' is required for action '{$value['action']}'"; + return false; + } + // Data must be array (can be empty) if (!\is_array($value['data'])) { $this->description = "Key 'data' must be an array"; From d444bb66b87aef4761a49a81897ef8298e09d6a3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:33:35 +1200 Subject: [PATCH 053/145] Truemaps for fast lookup --- .../Utopia/Database/Validator/Operation.php | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index ac585e26bc..25b9adcd9a 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -16,16 +16,24 @@ class Operation extends Validator 'data', ]; - /** @var array */ + /** @var array */ + private array $requiresDocumentId = [ + 'create' => true, + 'update' => true, + 'upsert' => true, + 'delete' => true, + ]; + + /** @var array */ private array $actions = [ - 'create', - 'update', - 'upsert', - 'delete', - 'bulkCreate', - 'bulkUpdate', - 'bulkUpsert', - 'bulkDelete', + 'create' => true, + 'update' => true, + 'upsert' => true, + 'delete' => true, + 'bulkCreate' => true, + 'bulkUpdate' => true, + 'bulkUpsert' => true, + 'bulkDelete' => true, ]; public function getDescription(): string @@ -66,7 +74,7 @@ class Operation extends Validator } // Validate action - if (!\in_array($value['action'], $this->actions, true)) { + if (!isset($this->actions[$value['action']])) { $this->description = "Key 'action' must be one of: " . \implode(', ', $this->actions); return false; } From 44daaae981c14147cf14d012137cd1fa8828793b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:33:44 +1200 Subject: [PATCH 054/145] Update deps --- composer.lock | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/composer.lock b/composer.lock index b97dbb2e7f..a0d261c518 100644 --- a/composer.lock +++ b/composer.lock @@ -1228,16 +1228,16 @@ }, { "name": "open-telemetry/context", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc" + "reference": "438f71812242db3f196fb4c717c6f92cbc819be6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc", - "reference": "4d5d98f1d4311a55b8d07e3d4c06d2430b4e6efc", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/438f71812242db3f196fb4c717c6f92cbc819be6", + "reference": "438f71812242db3f196fb4c717c6f92cbc819be6", "shasum": "" }, "require": { @@ -1283,7 +1283,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-04T03:25:06+00:00" + "time": "2025-08-13T01:12:00+00:00" }, { "name": "open-telemetry/exporter-otlp", @@ -3545,16 +3545,16 @@ }, { "name": "utopia-php/database", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "eaa4e275cefdeeb90bcece2f056e05b59f5b1473" + "reference": "33f35f5daeebd587f79abf362cfb512b9df3cd50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/eaa4e275cefdeeb90bcece2f056e05b59f5b1473", - "reference": "eaa4e275cefdeeb90bcece2f056e05b59f5b1473", + "url": "https://api.github.com/repos/utopia-php/database/zipball/33f35f5daeebd587f79abf362cfb512b9df3cd50", + "reference": "33f35f5daeebd587f79abf362cfb512b9df3cd50", "shasum": "" }, "require": { @@ -3595,9 +3595,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.0.0" + "source": "https://github.com/utopia-php/database/tree/1.0.1" }, - "time": "2025-08-11T13:56:31+00:00" + "time": "2025-08-13T12:28:06+00:00" }, { "name": "utopia-php/detector", @@ -5440,16 +5440,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -5468,7 +5468,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -5492,9 +5492,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2025-07-27T20:03:57+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", @@ -8397,18 +8397,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/database", - "version": "dev-feat-transaction-pinning", - "alias": "0.71.6", - "alias_normalized": "0.71.6.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/database": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { From 4b4969e79b3ce457a37c0f4710192bb8ca6a5f3f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:40:08 +1200 Subject: [PATCH 055/145] Sync response --- src/Appwrite/Utopia/Response.php | 188 +++++++++---------------------- 1 file changed, 54 insertions(+), 134 deletions(-) diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 1e8f9d5ee8..53f38b9016 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -161,6 +161,7 @@ class Response extends SwooleResponse public const MODEL_METRIC_LIST = 'metricList'; public const MODEL_METRIC_BREAKDOWN = 'metricBreakdown'; public const MODEL_ERROR_DEV = 'errorDev'; + public const MODEL_BASE_LIST = 'baseList'; public const MODEL_USAGE_DATABASES = 'usageDatabases'; public const MODEL_USAGE_DATABASE = 'usageDatabase'; public const MODEL_USAGE_TABLE = 'usageTable'; @@ -378,6 +379,13 @@ class Response extends SwooleResponse // Console public const MODEL_CONSOLE_VARIABLES = 'consoleVariables'; + // Deprecated + public const MODEL_PERMISSIONS = 'permissions'; + public const MODEL_RULE = 'rule'; + public const MODEL_TASK = 'task'; + public const MODEL_DOMAIN = 'domain'; + public const MODEL_DOMAIN_LIST = 'domainList'; + // Tests (keep last) public const MODEL_MOCK = 'mock'; @@ -441,56 +449,29 @@ class Response extends SwooleResponse ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) - ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) - ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) - ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) - ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) - ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) - ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) - ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) - ->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT)) ->setModel(new BaseList('Dev Keys List', self::MODEL_DEV_KEY_LIST, 'devKeys', self::MODEL_DEV_KEY, true, false)) - ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) - ->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION)) - ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('Framework Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_FRAMEWORK)) - ->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK)) - ->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION)) - ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) - ->setModel(new BaseList('Identities List', self::MODEL_IDENTITY_LIST, 'identities', self::MODEL_IDENTITY)) - ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) - ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) - ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) - ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) - ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) - ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) - ->setModel(new BaseList('Message List', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) - ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) - ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) - ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) + ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) - ->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) - ->setModel(new BaseList('Provider List', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) - ->setModel(new BaseList('Resource Tokens List', self::MODEL_RESOURCE_TOKEN_LIST, 'tokens', self::MODEL_RESOURCE_TOKEN)) - ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) - ->setModel(new BaseList('Runtime Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', self::MODEL_PROVIDER_REPOSITORY_RUNTIME)) - ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME)) - ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) - ->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE)) - ->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE)) - ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) + ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) + ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) + ->setModel(new BaseList('Languages List', self::MODEL_LANGUAGE_LIST, 'languages', self::MODEL_LANGUAGE)) + ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) + ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) + ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) + ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) + ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) + ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) + ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) + ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) + ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) - ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) - ->setModel(new BaseList('Topic List', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) ->setModel(new BaseList('Transaction List', self::MODEL_TRANSACTION_LIST, 'transactions', self::MODEL_TRANSACTION)) - ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) + ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) + ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) + ->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION)) ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) - ->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE)) - ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) // Entities ->setModel(new Database()) // Collection API Models @@ -531,75 +512,30 @@ class Response extends SwooleResponse ->setModel(new AlgoSha()) ->setModel(new AlgoPhpass()) ->setModel(new AlgoBcrypt()) - ->setModel(new AlgoMd5()) - ->setModel(new AlgoPhpass()) ->setModel(new AlgoScrypt()) ->setModel(new AlgoScryptModified()) - ->setModel(new AlgoSha()) - ->setModel(new Attribute()) - ->setModel(new AttributeBoolean()) - ->setModel(new AttributeDatetime()) - ->setModel(new AttributeEmail()) - ->setModel(new AttributeEnum()) - ->setModel(new AttributeFloat()) - ->setModel(new AttributeIP()) - ->setModel(new AttributeInteger()) - ->setModel(new AttributeList()) - ->setModel(new AttributeRelationship()) - ->setModel(new AttributeString()) - ->setModel(new AttributeURL()) - ->setModel(new AuthProvider()) - ->setModel(new Branch()) - ->setModel(new Bucket()) - ->setModel(new Collection()) - ->setModel(new ConsoleVariables()) - ->setModel(new Continent()) - ->setModel(new Country()) - ->setModel(new Currency()) - ->setModel(new Database()) - ->setModel(new Deployment()) - ->setModel(new DetectionFramework()) - ->setModel(new DetectionRuntime()) - ->setModel(new DevKey()) - ->setModel(new Execution()) - ->setModel(new File()) - ->setModel(new Framework()) - ->setModel(new FrameworkAdapter()) - ->setModel(new Func()) - ->setModel(new Headers()) - ->setModel(new HealthAntivirus()) - ->setModel(new HealthCertificate()) - ->setModel(new HealthQueue()) - ->setModel(new HealthStatus()) - ->setModel(new HealthTime()) - ->setModel(new HealthVersion()) + ->setModel(new AlgoArgon2()) + ->setModel(new Account()) + ->setModel(new Preferences()) + ->setModel(new Session()) ->setModel(new Identity()) - ->setModel(new Index()) - ->setModel(new Installation()) + ->setModel(new Token()) ->setModel(new JWT()) - ->setModel(new Key()) - ->setModel(new Language()) ->setModel(new Locale()) ->setModel(new LocaleCode()) - ->setModel(new Log()) - ->setModel(new MFAChallenge()) - ->setModel(new MFAFactors()) - ->setModel(new MFARecoveryCodes()) - ->setModel(new MFAType()) + ->setModel(new File()) + ->setModel(new Bucket()) + ->setModel(new ResourceToken()) + ->setModel(new Team()) ->setModel(new Membership()) - ->setModel(new Message()) - ->setModel(new Metric()) - ->setModel(new MetricBreakdown()) - ->setModel(new Migration()) - ->setModel(new MigrationFirebaseProject()) - ->setModel(new MigrationReport()) - ->setModel(new MockNumber()) - ->setModel(new ModelDocument()) - ->setModel(new Phone()) - ->setModel(new Platform()) - ->setModel(new Preferences()) - ->setModel(new Project()) - ->setModel(new Provider()) + ->setModel(new Site()) + ->setModel(new TemplateSite()) + ->setModel(new TemplateFramework()) + ->setModel(new Func()) + ->setModel(new TemplateFunction()) + ->setModel(new TemplateRuntime()) + ->setModel(new TemplateVariable()) + ->setModel(new Installation()) ->setModel(new ProviderRepository()) ->setModel(new ProviderRepositoryFramework()) ->setModel(new ProviderRepositoryRuntime()) @@ -648,38 +584,22 @@ class Response extends SwooleResponse ->setModel(new Headers()) ->setModel(new Specification()) ->setModel(new Rule()) - ->setModel(new Runtime()) - ->setModel(new Session()) - ->setModel(new Site()) - ->setModel(new Specification()) - ->setModel(new Subscriber()) - ->setModel(new Target()) - ->setModel(new Team()) - ->setModel(new TemplateEmail()) - ->setModel(new TemplateFramework()) - ->setModel(new TemplateFunction()) - ->setModel(new TemplateRuntime()) ->setModel(new TemplateSMS()) - ->setModel(new TemplateSite()) - ->setModel(new TemplateVariable()) - ->setModel(new Token()) + ->setModel(new TemplateEmail()) + ->setModel(new ConsoleVariables()) + ->setModel(new MFAChallenge()) + ->setModel(new MFARecoveryCodes()) + ->setModel(new MFAType()) + ->setModel(new MFAFactors()) + ->setModel(new Provider()) + ->setModel(new Message()) ->setModel(new Topic()) ->setModel(new Transaction()) - ->setModel(new UsageBuckets()) - ->setModel(new UsageCollection()) - ->setModel(new UsageDatabase()) - ->setModel(new UsageDatabases()) - ->setModel(new UsageFunction()) - ->setModel(new UsageFunctions()) - ->setModel(new UsageProject()) - ->setModel(new UsageSite()) - ->setModel(new UsageSites()) - ->setModel(new UsageStorage()) - ->setModel(new UsageUsers()) - ->setModel(new User()) - ->setModel(new Variable()) - ->setModel(new VcsContent()) - ->setModel(new Webhook()) + ->setModel(new Subscriber()) + ->setModel(new Target()) + ->setModel(new Migration()) + ->setModel(new MigrationReport()) + ->setModel(new MigrationFirebaseProject()) // Tests (keep last) ->setModel(new Mock()); From ef8d1b525d2761da3f6efa517084feac93b2efda Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 01:54:45 +1200 Subject: [PATCH 056/145] Fix missing grids injections --- .../Databases/Http/Grids/Tables/Rows/Column/Decrement.php | 1 + .../Databases/Http/Grids/Tables/Rows/Column/Increment.php | 1 + .../Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php | 1 + .../Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php | 1 + .../Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php | 1 + .../Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php | 1 + 6 files changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php index cb04e118ca..c5bcf5015e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Decrement.php @@ -65,6 +65,7 @@ class Decrement extends DecrementDocumentAttribute ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php index 4e36a2b912..0902a850a7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Column/Increment.php @@ -65,6 +65,7 @@ class Increment extends IncrementDocumentAttribute ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php index dbebfd28c4..cf0adddb89 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Create.php @@ -109,6 +109,7 @@ class Create extends DocumentCreate ->inject('queueForRealtime') ->inject('queueForFunctions') ->inject('queueForWebhooks') + ->inject('plan') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php index b13b17b8cd..ca1914b422 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Delete.php @@ -67,6 +67,7 @@ class Delete extends DocumentDelete ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php index c6ce1a9795..bed6c549f9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Update.php @@ -66,6 +66,7 @@ class Update extends DocumentUpdate ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php index 157956ca18..daf9147390 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Grids/Tables/Rows/Upsert.php @@ -69,6 +69,7 @@ class Upsert extends DocumentUpsert ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('plan') ->callback($this->action(...)); } } From 237263a194fc68385bbc0f56e54d496735abbf6e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 15 Aug 2025 02:01:44 +1200 Subject: [PATCH 057/145] Lint --- .../Collections/Documents/Attribute/Decrement.php | 4 ++-- .../Databases/Collections/Documents/Bulk/Update.php | 2 +- .../Http/Databases/Collections/Documents/Create.php | 4 ++-- .../Modules/Databases/Http/Transactions/Create.php | 2 +- .../Modules/Databases/Http/Transactions/Delete.php | 2 +- .../Modules/Databases/Http/Transactions/Get.php | 2 +- .../Databases/Http/Transactions/Operations/Create.php | 2 +- .../Modules/Databases/Http/Transactions/Update.php | 11 +++++------ .../Modules/Databases/Http/Transactions/XList.php | 2 +- 9 files changed, 15 insertions(+), 16 deletions(-) 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 1a6a7477af..7f67889a1c 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 @@ -87,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()); } 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 caaeaced1a..c198e9cc7b 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 @@ -109,7 +109,7 @@ class Update extends Action $hasRelationships = \array_filter( $collection->getAttribute('attributes', []), - fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP ); if ($hasRelationships) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 6c9a7ee228..d4a7e2cfda 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -402,7 +402,7 @@ class Create extends Action 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $isBulk ? null: $documentId, + 'documentId' => $isBulk ? null : $documentId, 'action' => $isBulk ? 'bulkCreate' : 'create', 'data' => $isBulk ? $documents : $documents[0], ]); @@ -476,7 +476,7 @@ class Create extends Action ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection $response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED); - + if ($isBulk) { $response->dynamic(new Document([ 'total' => count($documents), diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php index 25a16e19e3..2c98565568 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php @@ -69,4 +69,4 @@ class Create extends Action ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php index ae6f41fb2d..511f1140bd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Delete.php @@ -73,4 +73,4 @@ class Delete extends Action $response->noContent(); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php index 1a8286e658..12b8629b6a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Get.php @@ -66,4 +66,4 @@ class Get extends Action ->setStatusCode(SwooleResponse::STATUS_CODE_OK) ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php index 74f6337751..eac2b930eb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Operations/Create.php @@ -114,4 +114,4 @@ class Create extends Action ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 27251f3fd9..4775fd06c9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -15,7 +15,6 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Transaction as TransactionException; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -127,11 +126,11 @@ class Update extends Action $document = new Document($data); $dbForProject->createOrUpdateDocuments($collectionId, [$document]); break; - + case 'delete': $dbForProject->deleteDocument($collectionId, $documentId); break; - + case 'increment': $dbForProject->increaseDocumentAttribute( collection: $collectionId, @@ -141,7 +140,7 @@ class Update extends Action max: $data['max'] ?? null ); break; - + case 'decrement': $dbForProject->decreaseDocumentAttribute( collection: $collectionId, @@ -175,7 +174,7 @@ class Update extends Action } $dbForProject->createOrUpdateDocuments($collectionId, $documents); break; - + case 'bulkDelete': $dbForProject->deleteDocuments( $collectionId, @@ -224,4 +223,4 @@ class Update extends Action ->setStatusCode(SwooleResponse::STATUS_CODE_OK) ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php index ee58807d6f..bf8a33a793 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/XList.php @@ -70,4 +70,4 @@ class XList extends Action 'total' => $dbForProject->count('transactions', $queries), ]), UtopiaResponse::MODEL_TRANSACTION_LIST); } -} \ No newline at end of file +} From 14002dc20d38044ee9f7dcb67b3695aaacf9ebef Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 18 Aug 2025 17:37:16 +1200 Subject: [PATCH 058/145] Remove invalid return --- tests/e2e/Services/Databases/Legacy/DatabasesBase.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 580f9144b6..d652673370 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -4349,8 +4349,6 @@ trait DatabasesBase $this->assertEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); } - - return $data; } public function testUpdatePermissionsWithEmptyPayload(): array From c9cf38d6304bac6edf5ec62bdf0e7617f915abc5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 01:31:53 +1200 Subject: [PATCH 059/145] Add expired log deletion --- app/init/constants.php | 1 + composer.lock | 169 +++++++++++++----- .../Databases/Http/Transactions/Update.php | 1 + src/Appwrite/Platform/Workers/Deletes.php | 80 ++++++--- 4 files changed, 183 insertions(+), 68 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index 66933149a8..2e22a4ca3c 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -109,6 +109,7 @@ 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'; diff --git a/composer.lock b/composer.lock index 3bf17bc228..90bb2f12f6 100644 --- a/composer.lock +++ b/composer.lock @@ -2599,16 +2599,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1c064a0c67749923483216b081066642751cc2c7" + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", - "reference": "1c064a0c67749923483216b081066642751cc2c7", + "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", "shasum": "" }, "require": { @@ -2616,6 +2616,7 @@ "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -2674,7 +2675,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.2" + "source": "https://github.com/symfony/http-client/tree/v7.3.3" }, "funding": [ { @@ -2694,7 +2695,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-27T07:45:05+00:00" }, { "name": "symfony/http-client-contracts", @@ -2939,6 +2940,86 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -3557,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.2.3", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "8a536fead840d9da6ee819fe6b80e0f047997f69" + "reference": "87fb55e86892eecd726635eb1829acb743c2c156" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/8a536fead840d9da6ee819fe6b80e0f047997f69", - "reference": "8a536fead840d9da6ee819fe6b80e0f047997f69", + "url": "https://api.github.com/repos/utopia-php/database/zipball/87fb55e86892eecd726635eb1829acb743c2c156", + "reference": "87fb55e86892eecd726635eb1829acb743c2c156", "shasum": "" }, "require": { @@ -3607,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.2.3" + "source": "https://github.com/utopia-php/database/tree/1.2.4" }, - "time": "2025-08-27T11:47:04+00:00" + "time": "2025-09-01T06:01:09+00:00" }, { "name": "utopia-php/detector", @@ -4109,16 +4190,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569" + "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", - "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", + "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", "shasum": "" }, "require": { @@ -4159,9 +4240,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.0" + "source": "https://github.com/utopia-php/migration/tree/1.0.1" }, - "time": "2025-08-13T09:15:53+00:00" + "time": "2025-08-28T13:41:25+00:00" }, { "name": "utopia-php/orchestration", @@ -7410,16 +7491,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "shasum": "" }, "require": { @@ -7484,7 +7565,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.3" }, "funding": [ { @@ -7504,7 +7585,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/filesystem", @@ -7646,16 +7727,16 @@ }, { "name": "symfony/options-resolver", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", "shasum": "" }, "require": { @@ -7693,7 +7774,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" }, "funding": [ { @@ -7713,7 +7794,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-05T10:16:07+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8047,16 +8128,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", "shasum": "" }, "require": { @@ -8088,7 +8169,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.3" }, "funding": [ { @@ -8099,25 +8180,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-08-18T09:42:54+00:00" }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "shasum": "" }, "require": { @@ -8175,7 +8260,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.3" }, "funding": [ { @@ -8195,7 +8280,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "textalk/websocket", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 4775fd06c9..dfae4f3a50 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -193,6 +193,7 @@ class Update extends Action $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); + } catch (DuplicateException|ConflictException) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index c7dda8ccf0..b68c758169 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -86,14 +86,15 @@ class Deletes extends Action string $executionRetention, string $auditRetention, Log $log - ): void { + ): void + { $payload = $message->getPayload() ?? []; if (empty($payload)) { throw new Exception('Missing payload'); } - $type = $payload['type'] ?? ''; + $type = $payload['type'] ?? ''; $datetime = $payload['datetime'] ?? null; $hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null; $resource = $payload['resource'] ?? null; @@ -185,6 +186,7 @@ class Deletes extends Action $this->deleteAuditLogs($project, $getProjectDB, $auditRetention); $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); + $this->deleteExpiredTransactions($project, $getProjectDB); break; default: throw new \Exception('No delete operation for type: ' . \strval($type)); @@ -308,9 +310,9 @@ class Deletes extends Action * @param Document $project * @param callable $getProjectDB * @param string $resource + * @param string|null $resourceType * @return void * @throws Authorization - * @param string|null $resourceType * @throws Exception */ private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, string $resourceType = null): void @@ -398,7 +400,7 @@ class Deletes extends Action */ private function deleteUsageStats(Document $project, callable $getProjectDB, callable $getLogsDB, string $hourlyUsageRetentionDatetime): void { - /** @var Database $dbForProject*/ + /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); $selects = [...$this->selects, 'time']; @@ -413,7 +415,7 @@ class Deletes extends Action ], $dbForProject); if ($project->getId() !== 'console') { - /** @var Database $dbForLogs*/ + /** @var Database $dbForLogs */ $dbForLogs = call_user_func($getLogsDB, $project); // Delete Usage stats from logsDB @@ -455,16 +457,16 @@ class Deletes extends Action } /** - * @param Database $dbForPlatform - * @param Document $document - * @return void - * @throws Authorization - * @throws DatabaseException - * @throws Conflict - * @throws Restricted - * @throws Structure - * @throws Exception - */ + * @param Database $dbForPlatform + * @param Document $document + * @return void + * @throws Authorization + * @throws DatabaseException + * @throws Conflict + * @throws Restricted + * @throws Structure + * @throws Exception + */ protected function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void { @@ -542,7 +544,7 @@ class Deletes extends Action ); } } catch (Throwable $e) { - Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); + Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage()); } }); @@ -609,7 +611,7 @@ class Deletes extends Action ); } elseif ($sharedTablesV2) { $queries = \array_map( - fn ($id) => Query::notEqual('$id', $id), + fn($id) => Query::notEqual('$id', $id), $projectCollectionIds ); @@ -953,14 +955,14 @@ class Deletes extends Action } Console::info("Deleting screenshots for deployment " . $deployment->getId()); - $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + $bucket = ValidatorAuthorization::skip(fn() => $dbForPlatform->getDocument('buckets', 'screenshots')); if ($bucket->isEmpty()) { Console::error('Failed to get bucket for deployment screenshots'); return; } foreach ($screenshotIds as $id) { - $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id)); + $file = ValidatorAuthorization::skip(fn() => $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id)); if ($file->isEmpty()) { Console::error('Failed to get deployment screenshot: ' . $id); @@ -1117,12 +1119,10 @@ class Deletes extends Action array $queries, Database $database, ?callable $callback = null - ): void { + ): void + { $start = \microtime(true); - $deleteBatchSize = Database::DELETE_BATCH_SIZE; - $deleteBatchSize = 500; // TODO: Set right value in DB library after investigation - /** * deleteDocuments uses a cursor, we need to add a unique order by field or use default */ @@ -1130,11 +1130,10 @@ class Deletes extends Action $count = $database->deleteDocuments( $collection, $queries, - $deleteBatchSize, - $callback + onNext: $callback ); } catch (Throwable $th) { - $tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : ''; + $tenant = $database->getSharedTables() ? 'Tenant:' . $database->getTenant() : ''; Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collection} {$tenant} :{$th->getMessage()}"); return; } @@ -1318,4 +1317,33 @@ class Deletes extends Action Console::error("Failed to delete transaction logs for {$transactionId}: " . $th->getMessage()); } } + + private function deleteExpiredTransactions(Document $project, callable $getProjectDB): void + { + $dbForProject = $getProjectDB($project); + $transactionInternalIds = []; + + try { + $dbForProject->deleteDocuments('transactions', [ + Query::equal('status', ['pending']), + Query::lessThan('expiresAt', DateTime::format(new \DateTime())), + ], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) { + $transactionInternalIds[] = $transaction->getSequence(); + }, onError: function (Throwable $th) use ($project) { + // Swallow errors to avoid breaking the cleanup process + }); + } catch (Throwable $th) { + Console::error("Failed to find expired transactions for project {$project->getId()}: " . $th->getMessage()); + } + + if (empty($transactionInternalIds)) { + return; + } + + $dbForProject->deleteDocuments('transactionLogs', [ + Query::equal('transactionInternalId', $transactionInternalIds), + ], onError: function (Throwable $th) use ($project) { + // Swallow errors to avoid breaking the cleanup process + }); + } } From 3f0ad7f6c76d9da9bd3bbba6f8ce48ae9736aabb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 03:41:53 +1200 Subject: [PATCH 060/145] Update registry --- .../Modules/Databases/Services/Http.php | 12 +++---- .../Databases/Services/Registry/Databases.php | 31 ------------------- .../Registry/{Collections.php => Legacy.php} | 23 +++++++++++++- .../Registry/{Tables.php => TablesDB.php} | 2 +- .../Services/Registry/Transactions.php | 27 ++++++++++++++++ 5 files changed, 56 insertions(+), 39 deletions(-) delete mode 100644 src/Appwrite/Platform/Modules/Databases/Services/Registry/Databases.php rename src/Appwrite/Platform/Modules/Databases/Services/Registry/{Collections.php => Legacy.php} (87%) rename src/Appwrite/Platform/Modules/Databases/Services/Registry/{Tables.php => TablesDB.php} (99%) create mode 100644 src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Http.php b/src/Appwrite/Platform/Modules/Databases/Services/Http.php index ccd9dfd140..e5a2d92b40 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php @@ -3,9 +3,9 @@ namespace Appwrite\Platform\Modules\Databases\Services; use Appwrite\Platform\Modules\Databases\Http\Init\Timeout; -use Appwrite\Platform\Modules\Databases\Services\Registry\Collections as CollectionsRegistry; -use Appwrite\Platform\Modules\Databases\Services\Registry\Databases as DatabasesRegistry; -use Appwrite\Platform\Modules\Databases\Services\Registry\Tables as TablesRegistry; +use Appwrite\Platform\Modules\Databases\Services\Registry\Legacy as LegacyRegistry; +use Appwrite\Platform\Modules\Databases\Services\Registry\TablesDB as TablesDBRegistry; +use Appwrite\Platform\Modules\Databases\Services\Registry\Transactions as TransactionsRegistry; use Utopia\Platform\Service; class Http extends Service @@ -17,9 +17,9 @@ class Http extends Service $this->addAction(Timeout::getName(), new Timeout()); foreach ([ - DatabasesRegistry::class, - CollectionsRegistry::class, - TablesRegistry::class, + LegacyRegistry::class, + TablesDBRegistry::class, + TransactionsRegistry::class, ] as $registrar) { new $registrar($this); } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Databases.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Databases.php deleted file mode 100644 index 81c9174253..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Databases.php +++ /dev/null @@ -1,31 +0,0 @@ -addAction(CreateDatabase::getName(), new CreateDatabase()); - $service->addAction(GetDatabase::getName(), new GetDatabase()); - $service->addAction(UpdateDatabase::getName(), new UpdateDatabase()); - $service->addAction(DeleteDatabase::getName(), new DeleteDatabase()); - $service->addAction(ListDatabases::getName(), new ListDatabases()); - $service->addAction(ListDatabaseLogs::getName(), new ListDatabaseLogs()); - $service->addAction(GetDatabaseUsage::getName(), new GetDatabaseUsage()); - $service->addAction(ListDatabaseUsage::getName(), new ListDatabaseUsage()); - } -} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php similarity index 87% rename from src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php rename to src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php index bb0002ed47..ad51178197 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Collections.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php @@ -48,6 +48,14 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Update as UpdateCollection; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Usage\Get as GetCollectionUsage; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\XList as ListCollections; +use Appwrite\Platform\Modules\Databases\Http\Databases\Create as CreateDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Delete as DeleteDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Get as GetDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Logs\XList as ListDatabaseLogs; +use Appwrite\Platform\Modules\Databases\Http\Databases\Update as UpdateDatabase; +use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\Get as GetDatabaseUsage; +use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\XList as ListDatabaseUsage; +use Appwrite\Platform\Modules\Databases\Http\Databases\XList as ListDatabases; use Utopia\Platform\Service; /** @@ -59,16 +67,29 @@ use Utopia\Platform\Service; * - Attributes * - Indexes */ -class Collections extends Base +class Legacy extends Base { protected function register(Service $service): void { + $this->registerDatabaseActions($service); $this->registerCollectionActions($service); $this->registerDocumentActions($service); $this->registerAttributeActions($service); $this->registerIndexActions($service); } + public function registerDatabaseActions(Service $service): void + { + $service->addAction(CreateDatabase::getName(), new CreateDatabase()); + $service->addAction(GetDatabase::getName(), new GetDatabase()); + $service->addAction(UpdateDatabase::getName(), new UpdateDatabase()); + $service->addAction(DeleteDatabase::getName(), new DeleteDatabase()); + $service->addAction(ListDatabases::getName(), new ListDatabases()); + $service->addAction(ListDatabaseLogs::getName(), new ListDatabaseLogs()); + $service->addAction(GetDatabaseUsage::getName(), new GetDatabaseUsage()); + $service->addAction(ListDatabaseUsage::getName(), new ListDatabaseUsage()); + } + private function registerCollectionActions(Service $service): void { $service->addAction(CreateCollection::getName(), new CreateCollection()); diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php similarity index 99% rename from src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php rename to src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php index 7772aff933..4d0456d9c0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Tables.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php @@ -66,7 +66,7 @@ use Utopia\Platform\Service; * - Columns * - Column-Indexes */ -class Tables extends Base +class TablesDB extends Base { protected function register(Service $service): void { diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php new file mode 100644 index 0000000000..3b02ffaec0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php @@ -0,0 +1,27 @@ +addAction(CreateTransaction::getName(), new CreateTransaction()); + $service->addAction(GetTransaction::getName(), new GetTransaction()); + $service->addAction(UpdateTransaction::getName(), new UpdateTransaction()); + $service->addAction(DeleteTransaction::getName(), new DeleteTransaction()); + $service->addAction(ListTransactions::getName(), new ListTransactions()); + $service->addAction(CreateOperations::getName(), new CreateOperations()); + } +} \ No newline at end of file From 70ef7059c309db4d3e4d5738606116aa6f6cb33f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 03:42:19 +1200 Subject: [PATCH 061/145] Add increment validation --- .../Utopia/Database/Validator/Operation.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 25b9adcd9a..594648db3e 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -13,7 +13,6 @@ class Operation extends Validator 'databaseId', 'collectionId', 'action', - 'data', ]; /** @var array */ @@ -22,6 +21,8 @@ class Operation extends Validator 'update' => true, 'upsert' => true, 'delete' => true, + 'increment' => true, + 'decrement' => true, ]; /** @var array */ @@ -30,6 +31,8 @@ class Operation extends Validator 'update' => true, 'upsert' => true, 'delete' => true, + 'increment' => true, + 'decrement' => true, 'bulkCreate' => true, 'bulkUpdate' => true, 'bulkUpsert' => true, @@ -75,7 +78,7 @@ class Operation extends Validator // Validate action if (!isset($this->actions[$value['action']])) { - $this->description = "Key 'action' must be one of: " . \implode(', ', $this->actions); + $this->description = "Key 'action' must be one of: " . \implode(', ', array_keys($this->actions)); return false; } @@ -88,7 +91,11 @@ class Operation extends Validator return false; } - // Data must be array (can be empty) + // Data must be present and must be array (can be empty) + if (!\array_key_exists('data', $value)) { + $this->description = "Missing required key: data"; + return false; + } if (!\is_array($value['data'])) { $this->description = "Key 'data' must be an array"; return false; From 7861ee8cbda4eedcdfcea8b304607a63397e5f09 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 03:42:44 +1200 Subject: [PATCH 062/145] Add transaction scopes to test keys --- tests/e2e/Scopes/ProjectCustom.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index c2b4896814..49aabaae67 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -70,6 +70,8 @@ trait ProjectCustom 'databases.write', 'collections.read', 'collections.write', + 'transactions.read', + 'transactions.write', 'tables.read', 'tables.write', 'documents.read', From 04a9ff9c2305344fd172df6df3bf974a7137eee7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 03:42:56 +1200 Subject: [PATCH 063/145] Ensure ID on create doc op --- .../Platform/Modules/Databases/Http/Transactions/Update.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index dfae4f3a50..8282e61ef4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -88,7 +88,7 @@ class Update extends Action } if ($commit) { - $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, $transaction) { + $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, &$transaction) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', ])); @@ -113,6 +113,9 @@ class Update extends Action $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $queueForDeletes, $action, $collectionId, $documentId, $data) { switch ($action) { case 'create': + if ($documentId && !isset($data['$id'])) { + $data['$id'] = $documentId; + } $document = new Document($data); $dbForProject->createDocument($collectionId, $document); break; From 5528e9964bb76a03bc667da5327d64d5e22d84f4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 03:53:08 +1200 Subject: [PATCH 064/145] Updates tests --- .../Transactions/ACIDComplianceTest.php | 628 ++++++++++++++++++ .../DatabasesTransactionsTest.php | 363 ++++++++++ 2 files changed, 991 insertions(+) create mode 100644 tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php create mode 100644 tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php diff --git a/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php b/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php new file mode 100644 index 0000000000..4c0b0eecc3 --- /dev/null +++ b/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php @@ -0,0 +1,628 @@ +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' => 'AtomicityTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with unique constraint + $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' => 'AtomicityTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add unique attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'email', + 'size' => 256, + 'required' => true, + ]); + + // Add unique index + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'unique_email', + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['email'] + ]); + + sleep(3); + + // Create first document outside transaction + $doc1 = $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' => ID::unique(), + 'data' => [ + 'email' => 'existing@example.com' + ] + ]); + + $this->assertEquals(201, $doc1['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction)); + $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body'])); + $transactionId = $transaction['body']['$id']; + + // Add operations - second one will fail due to unique constraint + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'newuser@example.com' // This should succeed + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'existing@example.com' // This will fail - duplicate + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'anotheruser@example.com' // This should not be created due to atomicity + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code'], 'Add operations failed. Response: ' . json_encode($response['body'])); + + // Attempt to commit - should fail due to unique constraint violation + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + if ($response['headers']['status-code'] === 200) { + // If transaction succeeded, all documents should be created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have 4 documents total (1 original + 3 from transaction) + // But since we have a unique constraint violation, this might fail + $this->assertGreaterThanOrEqual(1, $documents['body']['total']); + } else { + $this->assertEquals(409, $response['headers']['status-code']); // Conflict error + + // Verify NO new documents were created (atomicity) + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(1, $documents['body']['total']); // Only the original document + $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + } + } + + /** + * Test consistency - schema validation and constraints + */ + public function testTransactionConsistency(): void + { + // Create database + $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' => 'ConsistencyTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with required fields and constraints + $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' => 'ConsistencyTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add required string attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'required_field', + 'size' => 256, + 'required' => true, + ]); + + // Add integer attribute with min/max constraints + $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' => 'age', + 'required' => true, + 'min' => 18, + 'max' => 100 + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operations with both valid and invalid data + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Valid User', + 'age' => 25 // Valid age + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Too Young User', + 'age' => 10 // Below minimum - will fail constraint + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Another Valid User', + 'age' => 30 // Valid but should not be created due to transaction failure + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Attempt to commit - should fail due to constraint violation + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertContains($response['headers']['status-code'], [400, 500], 'Transaction commit should fail due to validation. Response: ' . json_encode($response['body'])); + + // Verify no documents were created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(0, $documents['body']['total']); + } + + /** + * Test isolation - concurrent transactions on same data + */ + public function testTransactionIsolation(): void + { + // Create database + $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' => 'IsolationTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'IsolationTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add counter 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' => 'counter', + 'required' => true, + 'min' => 0, + 'max' => 1000000 + ]); + + sleep(2); + + // Create initial document with counter + $doc = $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' => 'shared_counter', + 'data' => [ + 'counter' => 0 + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create first transaction + $transaction1 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed'); + $this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id'); + $transactionId1 = $transaction1['body']['$id']; + + // Create second transaction + $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed'); + $this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id'); + $transactionId2 = $transaction2['body']['$id']; + + // Transaction 1: Increment counter by 10 + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId1}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'shared_counter', + 'action' => 'increment', + 'data' => [ + 'attribute' => 'counter', + 'value' => 10 + ] + ] + ] + ]); + + // Transaction 2: Increment counter by 5 + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'shared_counter', + 'action' => 'increment', + 'data' => [ + 'attribute' => 'counter', + 'value' => 5 + ] + ] + ] + ]); + + // Commit first transaction + $response1 = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId1}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response1['headers']['status-code']); + + // Commit second transaction + $response2 = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response2['headers']['status-code']); + + // Check final value - both increments should be applied + $document = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/shared_counter", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Both increments should be applied: 0 + 10 + 5 = 15 + $this->assertEquals(15, $document['body']['counter']); + } + + /** + * Test durability - committed data persists + */ + public function testTransactionDurability(): void + { + // Create database + $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' => 'DurabilityTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'DurabilityTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create and commit transaction with multiple operations + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed'); + $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id'); + $transactionId = $transaction['body']['$id']; + + // Add multiple operations + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'durable_doc_1', + 'data' => [ + 'data' => 'Important data 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'durable_doc_2', + 'data' => [ + 'data' => 'Important data 2' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'durable_doc_1', + 'data' => [ + 'data' => 'Updated important data 1' + ] + ] + ] + ]); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body'])); + $this->assertEquals('committed', $response['body']['status']); + + // List all documents to see what was created + $allDocs = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body'])); + + // Verify documents exist and have correct data + $document1 = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $document1['headers']['status-code']); + $this->assertEquals('Updated important data 1', $document1['body']['data']); + + $document2 = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/durable_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $document2['headers']['status-code']); + $this->assertEquals('Important data 2', $document2['body']['data']); + + // Further update outside transaction to ensure persistence + $update = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'data' => 'Modified outside transaction' + ] + ]); + + $this->assertEquals(200, $update['headers']['status-code']); + + // Verify the update persisted + $document1 = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Modified outside transaction', $document1['body']['data']); + + // List all documents to verify total count + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(2, $documents['body']['total']); + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php new file mode 100644 index 0000000000..35340f8b0e --- /dev/null +++ b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php @@ -0,0 +1,363 @@ +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' => 'TransactionTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Test creating a transaction with default TTL + $response = $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, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('status', $response['body']); + $this->assertArrayHasKey('operations', $response['body']); + $this->assertArrayHasKey('expiresAt', $response['body']); + $this->assertEquals('pending', $response['body']['status']); + $this->assertEquals(0, $response['body']['operations']); + + $transactionId1 = $response['body']['$id']; + + // Test creating a transaction with custom TTL + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 900 + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals('pending', $response['body']['status']); + + $expiresAt = new \DateTime($response['body']['expiresAt']); + $now = new \DateTime(); + $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThan(800, $diff); + $this->assertLessThan(1000, $diff); + + $transactionId2 = $response['body']['$id']; + + // Test invalid TTL values + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 30 // Below minimum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 4000 // Above maximum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return [ + 'databaseId' => $databaseId, + 'transactionId1' => $transactionId1, + 'transactionId2' => $transactionId2 + ]; + } + + /** + * @depends testCreate + */ + public function testAddOperations(array $data): array + { + $databaseId = $data['databaseId']; + $transactionId = $data['transactionId1']; + + // Create a collection for testing + $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' => 'TransactionOperationsTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add attributes + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + + // Wait for attribute to be created + sleep(2); + + // Add valid operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + '$id' => 'doc1', + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + '$id' => 'doc2', + 'name' => 'Test Document 2' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['operations']); + + // Test adding more operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); + + // Test invalid database ID + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => 'invalid_database', + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Test invalid collection ID + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => 'invalid_collection', + 'action' => 'create', + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + return array_merge($data, [ + 'collectionId' => $collectionId + ]); + } + + /** + * @depends testAddOperations + */ + public function testCommit(array $data): void + { + $databaseId = $data['databaseId']; + $collectionId = $data['collectionId']; + $transactionId = $data['transactionId1']; + + // Commit the transaction + $response = $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, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify documents were created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(2, $documents['body']['total']); + + // Verify the update was applied + $doc1Found = false; + foreach ($documents['body']['documents'] as $doc) { + if ($doc['$id'] === 'doc1') { + $this->assertEquals('Updated Document 1', $doc['name']); + $doc1Found = true; + } + } + $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); + + // Test committing already committed transaction + $response = $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(400, $response['headers']['status-code']); + } + + /** + * @depends testCreate + */ + public function testRollback(array $data): void + { + $databaseId = $data['databaseId']; + $transactionId = $data['transactionId2']; + + // Create a collection for rollback test + $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' => 'TransactionRollbackTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'data' => [ + '$id' => 'rollback_doc', + 'value' => 'Should not exist' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Rollback the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('rolledBack', $response['body']['status']); + + // Verify no documents were created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(0, $documents['body']['total']); + } +} \ No newline at end of file From 2202dc9747864db0acf75a5f2215ab06955b8035 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 04:29:42 +1200 Subject: [PATCH 065/145] Handle intra-transactional ops with state tracking --- .../Databases/Http/Transactions/Update.php | 195 ++++++++++++------ .../Services/Registry/Transactions.php | 4 +- src/Appwrite/Platform/Workers/Deletes.php | 12 +- .../Transactions/ACIDComplianceTest.php | 8 +- .../DatabasesTransactionsTest.php | 42 ++-- 5 files changed, 171 insertions(+), 90 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 8282e61ef4..58ec470bd2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -93,13 +93,16 @@ class Update extends Action 'status' => 'committing', ])); - // Fetch operations ordered by sequence by default + // Fetch operations ordered by sequence by default to + // replay operations in exact order they were created $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), ]); + // Track transaction state for cross-operation visibility + $state = []; + try { - // Replay operations in exact order they were created foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; $collectionInternalId = $operation['collectionInternalId']; @@ -109,33 +112,28 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; - // Wrap each operation with the timestamp from when it was logged - $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $queueForDeletes, $action, $collectionId, $documentId, $data) { + // Check if this operation depends on documents created in same transaction + $dependent = \in_array($action, ['update', 'increment', 'decrement']) + && isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Don't use timestamp wrapper for dependent operations switch ($action) { - case 'create': - if ($documentId && !isset($data['$id'])) { - $data['$id'] = $documentId; - } - $document = new Document($data); - $dbForProject->createDocument($collectionId, $document); - break; - case 'update': - $document = new Document($data); - $dbForProject->updateDocument($collectionId, $documentId, $document); - break; - - case 'upsert': - $document = new Document($data); - $dbForProject->createOrUpdateDocuments($collectionId, [$document]); - break; - - case 'delete': - $dbForProject->deleteDocument($collectionId, $documentId); + // Update the state document directly + $existing = $state[$collectionId][$documentId]; + foreach ($data as $key => $value) { + $existing->setAttribute($key, $value); + } + $state[$collectionId][$documentId] = $dbForProject->updateDocument( + $collectionId, + $documentId, + $existing + ); break; case 'increment': - $dbForProject->increaseDocumentAttribute( + $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( collection: $collectionId, id: $documentId, attribute: $data['attribute'], @@ -145,7 +143,7 @@ class Update extends Action break; case 'decrement': - $dbForProject->decreaseDocumentAttribute( + $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( collection: $collectionId, id: $documentId, attribute: $data['attribute'], @@ -153,51 +151,126 @@ class Update extends Action min: $data['min'] ?? null ); break; - - case 'bulkCreate': - $documents = []; - foreach ($data as $docData) { - $documents[] = new Document($docData); - } - $dbForProject->createDocuments($collectionId, $documents); - break; - - case 'bulkUpdate': - $dbForProject->updateDocuments( - $collectionId, - $data['data'] ?? null, - Query::parseQueries($data['queries'] ?? []) - ); - break; - - case 'bulkUpsert': - $documents = []; - foreach ($data as $docData) { - $documents[] = new Document($docData); - } - $dbForProject->createOrUpdateDocuments($collectionId, $documents); - break; - - case 'bulkDelete': - $dbForProject->deleteDocuments( - $collectionId, - Query::parseQueries($data['queries'] ?? []) - ); - break; } - }); + } else { + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $queueForDeletes, $action, $collectionId, $documentId, $data, &$state) { + switch ($action) { + case 'create': + if ($documentId && !isset($data['$id'])) { + $data['$id'] = $documentId; + } + $state[$collectionId][$documentId] = $dbForProject->createDocument( + $collectionId, + new Document($data), + ); + break; + + case 'update': + $document = new Document($data); + $state[$collectionId][$documentId] = $dbForProject->updateDocument( + $collectionId, + $documentId, + $document, + ); + break; + + case 'upsert': + $document = new Document($data); + $dbForProject->createOrUpdateDocuments( + $collectionId, + [$document], + onNext: function (Document $document) use (&$state, $collectionId) { + $state[$collectionId][$document->getId()] = $document; + } + ); + break; + + case 'delete': + $dbForProject->deleteDocument($collectionId, $documentId); + + if (isset($state[$collectionId][$documentId])) { + unset($state[$collectionId][$documentId]); + } + break; + + case 'increment': + $dbForProject->increaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + max: $data['max'] ?? null + ); + break; + + case 'decrement': + $dbForProject->decreaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + min: $data['min'] ?? null + ); + break; + + case 'bulkCreate': + $dbForProject->createDocuments( + $collectionId, + array_map(fn ($data) => new Document($data), $data), + onNext: function (Document $document) use (&$state, $collectionId) { + $state[$collectionId][$document->getId()] = $document; + + } + ); + break; + + case 'bulkUpdate': + $dbForProject->updateDocuments( + $collectionId, + $data['data'] ?? null, + Query::parseQueries($data['queries'] ?? []) + ); + break; + + case 'bulkUpsert': + $dbForProject->createOrUpdateDocuments( + $collectionId, + array_map(fn ($data) => new Document($data), $data), + onNext: function (Document $document) use (&$state, $collectionId) { + $state[$collectionId][$document->getId()] = $document; + + } + ); + break; + + case 'bulkDelete': + $dbForProject->deleteDocuments( + $collectionId, + Query::parseQueries($data['queries'] ?? []) + ); + break; + } + }); + } } - $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committed', - ])); + $transaction = $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document( + [ + 'status' => 'committed', + ] + ) + ); // Clear the transaction logs $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); - } catch (DuplicateException|ConflictException) { + } catch (DuplicateException|ConflictException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php index 3b02ffaec0..b41d6adcdb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php @@ -5,9 +5,9 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry; use Appwrite\Platform\Modules\Databases\Http\Transactions\Create as CreateTransaction; use Appwrite\Platform\Modules\Databases\Http\Transactions\Delete as DeleteTransaction; use Appwrite\Platform\Modules\Databases\Http\Transactions\Get as GetTransaction; +use Appwrite\Platform\Modules\Databases\Http\Transactions\Operations\Create as CreateOperations; use Appwrite\Platform\Modules\Databases\Http\Transactions\Update as UpdateTransaction; use Appwrite\Platform\Modules\Databases\Http\Transactions\XList as ListTransactions; -use Appwrite\Platform\Modules\Databases\Http\Transactions\Operations\Create as CreateOperations; use Utopia\Platform\Service; /** @@ -24,4 +24,4 @@ class Transactions extends Base $service->addAction(ListTransactions::getName(), new ListTransactions()); $service->addAction(CreateOperations::getName(), new CreateOperations()); } -} \ No newline at end of file +} diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index b68c758169..b647b57908 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -86,8 +86,7 @@ class Deletes extends Action string $executionRetention, string $auditRetention, Log $log - ): void - { + ): void { $payload = $message->getPayload() ?? []; if (empty($payload)) { @@ -611,7 +610,7 @@ class Deletes extends Action ); } elseif ($sharedTablesV2) { $queries = \array_map( - fn($id) => Query::notEqual('$id', $id), + fn ($id) => Query::notEqual('$id', $id), $projectCollectionIds ); @@ -955,14 +954,14 @@ class Deletes extends Action } Console::info("Deleting screenshots for deployment " . $deployment->getId()); - $bucket = ValidatorAuthorization::skip(fn() => $dbForPlatform->getDocument('buckets', 'screenshots')); + $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); if ($bucket->isEmpty()) { Console::error('Failed to get bucket for deployment screenshots'); return; } foreach ($screenshotIds as $id) { - $file = ValidatorAuthorization::skip(fn() => $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id)); + $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $id)); if ($file->isEmpty()) { Console::error('Failed to get deployment screenshot: ' . $id); @@ -1119,8 +1118,7 @@ class Deletes extends Action array $queries, Database $database, ?callable $callback = null - ): void - { + ): void { $start = \microtime(true); /** diff --git a/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php b/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php index 4c0b0eecc3..42a2967685 100644 --- a/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php +++ b/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php @@ -7,12 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; use Utopia\Database\Database; -use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Database\Query; class ACIDComplianceTest extends Scope { @@ -162,7 +160,7 @@ class ACIDComplianceTest extends Scope $this->assertGreaterThanOrEqual(1, $documents['body']['total']); } else { $this->assertEquals(409, $response['headers']['status-code']); // Conflict error - + // Verify NO new documents were created (atomicity) $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ 'content-type' => 'application/json', @@ -577,7 +575,7 @@ class ACIDComplianceTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - + $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body'])); // Verify documents exist and have correct data @@ -625,4 +623,4 @@ class ACIDComplianceTest extends Scope $this->assertEquals(2, $documents['body']['total']); } -} \ No newline at end of file +} diff --git a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php index 35340f8b0e..1ed8e6963b 100644 --- a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php @@ -37,7 +37,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); @@ -53,13 +54,14 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'ttl' => 900 ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals('pending', $response['body']['status']); - + $expiresAt = new \DateTime($response['body']['expiresAt']); $now = new \DateTime(); $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); @@ -72,7 +74,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'ttl' => 30 // Below minimum ]); @@ -81,7 +84,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'ttl' => 4000 // Above maximum ]); @@ -142,7 +146,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'operations' => [ [ 'databaseId' => $databaseId, @@ -174,7 +179,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'operations' => [ [ 'databaseId' => $databaseId, @@ -195,7 +201,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'operations' => [ [ 'databaseId' => 'invalid_database', @@ -212,7 +219,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'operations' => [ [ 'databaseId' => $databaseId, @@ -243,7 +251,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'commit' => true ]); @@ -258,7 +267,7 @@ class DatabasesTransactionsTest extends Scope $this->assertEquals(200, $documents['headers']['status-code']); $this->assertEquals(2, $documents['body']['total']); - + // Verify the update was applied $doc1Found = false; foreach ($documents['body']['documents'] as $doc) { @@ -273,7 +282,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'commit' => true ]); @@ -324,7 +334,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'operations' => [ [ 'databaseId' => $databaseId, @@ -344,7 +355,8 @@ class DatabasesTransactionsTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ 'rollback' => true ]); @@ -360,4 +372,4 @@ class DatabasesTransactionsTest extends Scope $this->assertEquals(200, $documents['headers']['status-code']); $this->assertEquals(0, $documents['body']['total']); } -} \ No newline at end of file +} From 1d8d757960ed8a252d40e1dd6b83d6bfc137d755 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 04:32:59 +1200 Subject: [PATCH 066/145] Add txn to workflow --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cebdc02163..75fb2cce26 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -169,6 +169,7 @@ jobs: Console, Databases/Legacy, Databases/TablesDB, + Databases/Transactions, Functions, FunctionsSchedule, GraphQL, From 76287beca038f58c2bbdd79a8a9c5127ce674055 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 04:36:34 +1200 Subject: [PATCH 067/145] Format --- app/config/collections/projects.php | 170 ++++++++++++++-------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index e48dcef451..7bd1d4a7d8 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2524,138 +2524,138 @@ return [ 'transactions' => [ '$collection' => ID::custom(Database::METADATA), - '$id' => ID::custom('transactions'), - 'name' => 'Transactions', - 'attributes' => [ + '$id' => ID::custom('transactions'), + 'name' => 'Transactions', + 'attributes' => [ [ - '$id' => ID::custom('status'), - 'type' => Database::VAR_STRING, - 'size' => 16, // pending | committing | committed | rolled_back | failed - 'signed' => true, + '$id' => ID::custom('status'), + 'type' => Database::VAR_STRING, + 'size' => 16, // pending | committing | committed | rolled_back | failed + 'signed' => true, 'required' => false, - 'default' => 'pending', - 'array' => false, - 'filters' => [], + 'default' => 'pending', + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('operations'), - 'type' => Database::VAR_INTEGER, - 'size' => 0, - 'signed' => false, + '$id' => ID::custom('operations'), + 'type' => Database::VAR_INTEGER, + 'size' => 0, + 'signed' => false, 'required' => true, - 'default' => 0, - 'array' => false, - 'filters' => [], + 'default' => 0, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('expiresAt'), - 'type' => Database::VAR_DATETIME, - 'size' => 0, - 'signed' => true, + '$id' => ID::custom('expiresAt'), + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'signed' => true, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], ], ], 'indexes' => [ [ - '$id' => ID::custom('_key_status'), - 'type' => Database::INDEX_KEY, + '$id' => ID::custom('_key_status'), + 'type' => Database::INDEX_KEY, 'attributes' => ['status'], - 'lengths' => [], - 'orders' => [Database::ORDER_ASC], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_expires'), - 'type' => Database::INDEX_KEY, + '$id' => ID::custom('_key_expires'), + 'type' => Database::INDEX_KEY, 'attributes' => ['expiresAt'], - 'lengths' => [], - 'orders' => [Database::ORDER_DESC], + 'lengths' => [], + 'orders' => [Database::ORDER_DESC], ], ], ], 'transactionLogs' => [ '$collection' => ID::custom(Database::METADATA), - '$id' => ID::custom('transactionLogs'), - 'name' => 'Transaction Logs', - 'attributes' => [ + '$id' => ID::custom('transactionLogs'), + 'name' => 'Transaction Logs', + 'attributes' => [ [ - '$id' => ID::custom('transactionInternalId'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'signed' => true, + '$id' => ID::custom('transactionInternalId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], + 'default' => null, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('databaseInternalId'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'signed' => true, + '$id' => ID::custom('databaseInternalId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], + 'default' => null, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('collectionInternalId'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'signed' => true, + '$id' => ID::custom('collectionInternalId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], + 'default' => null, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('documentId'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'signed' => true, + '$id' => ID::custom('documentId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], + 'default' => null, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('action'), - 'type' => Database::VAR_STRING, - 'size' => 32, // create | update | upsert | increment | decrement | delete - 'signed' => true, + '$id' => ID::custom('action'), + 'type' => Database::VAR_STRING, + 'size' => 32, // create | update | upsert | increment | decrement | delete + 'signed' => true, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], + 'default' => null, + 'array' => false, + 'filters' => [], ], [ - '$id' => ID::custom('data'), - 'type' => Database::VAR_STRING, - 'size' => 65535, - 'signed' => false, + '$id' => ID::custom('data'), + 'type' => Database::VAR_STRING, + 'size' => 65535, + 'signed' => false, 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => ['json'], + 'default' => null, + 'array' => false, + 'filters' => ['json'], ], ], 'indexes' => [ [ - '$id' => ID::custom('_key_transaction'), - 'type' => Database::INDEX_KEY, + '$id' => ID::custom('_key_transaction'), + 'type' => Database::INDEX_KEY, 'attributes' => ['transactionInternalId'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [], + 'orders' => [], ], [ - '$id' => ID::custom('_key_internal_path'), - 'type' => Database::INDEX_KEY, + '$id' => ID::custom('_key_internal_path'), + 'type' => Database::INDEX_KEY, 'attributes' => ['databaseInternalId', 'collectionInternalId'], - 'lengths' => [], - 'orders' => [], + 'lengths' => [], + 'orders' => [], ], ], ], From c34efeff1ebc30d1255db93b7feda2ac987f6f30 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 04:37:48 +1200 Subject: [PATCH 068/145] Add to the other workflow --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 75fb2cce26..2ca0c58db8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -249,6 +249,7 @@ jobs: Console, Databases/Legacy, Databases/TablesDB, + Databases/Transactions, Functions, FunctionsSchedule, GraphQL, From 00f91c444c47b6b30acc7f04cba44ae01880380f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 04:55:31 +1200 Subject: [PATCH 069/145] Fix tests --- .../Databases/Transactions/DatabasesTransactionsTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php index 1ed8e6963b..513051fcc0 100644 --- a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php @@ -155,7 +155,6 @@ class DatabasesTransactionsTest extends Scope 'action' => 'create', 'documentId' => 'doc1', 'data' => [ - '$id' => 'doc1', 'name' => 'Test Document 1' ] ], @@ -165,7 +164,6 @@ class DatabasesTransactionsTest extends Scope 'action' => 'create', 'documentId' => 'doc2', 'data' => [ - '$id' => 'doc2', 'name' => 'Test Document 2' ] ] @@ -208,12 +206,13 @@ class DatabasesTransactionsTest extends Scope 'databaseId' => 'invalid_database', 'collectionId' => $collectionId, 'action' => 'create', + 'documentId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); - $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); // Test invalid collection ID $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ @@ -226,6 +225,7 @@ class DatabasesTransactionsTest extends Scope 'databaseId' => $databaseId, 'collectionId' => 'invalid_collection', 'action' => 'create', + 'documentId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] @@ -341,8 +341,8 @@ class DatabasesTransactionsTest extends Scope 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'create', + 'documentId' => 'rollback_doc', 'data' => [ - '$id' => 'rollback_doc', 'value' => 'Should not exist' ] ] From 903e9b52c23ba7ee2b97227eae889d5245fe6b74 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Sep 2025 23:54:21 +1200 Subject: [PATCH 070/145] Fix size limit exception --- app/config/errors.php | 4 +- app/controllers/general.php | 3 +- app/init/constants.php | 1 + src/Appwrite/Extend/Exception.php | 502 +++++++++--------- .../Databases/Http/Transactions/Update.php | 6 + 5 files changed, 265 insertions(+), 251 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index fa45550ba9..098c87c574 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -988,8 +988,8 @@ return [ ], Exception::TRANSACTION_LIMIT_EXCEEDED => [ 'name' => Exception::TRANSACTION_LIMIT_EXCEEDED, - 'description' => 'The maximum number of transactions has been reached.', - 'code' => 400, + 'description' => 'The maximum number of operations per transaction has been exceeded.', + 'code' => 422, ], Exception::TRANSACTION_NOT_READY => [ 'name' => Exception::TRANSACTION_NOT_READY, diff --git a/app/controllers/general.php b/app/controllers/general.php index 40ce66b574..a87aa45c4f 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1231,7 +1231,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())) { @@ -1333,6 +1333,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 diff --git a/app/init/constants.php b/app/init/constants.php index 2e22a4ca3c..2a068eba97 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -56,6 +56,7 @@ 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'; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 558f402ea2..cfa55c540e 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -36,341 +36,341 @@ class Exception extends \Exception */ /** General */ - public const GENERAL_UNKNOWN = 'general_unknown'; - public const GENERAL_MOCK = 'general_mock'; - public const GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden'; - public const GENERAL_RESOURCE_BLOCKED = 'general_resource_blocked'; - public const GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin'; - public const GENERAL_API_DISABLED = 'general_api_disabled'; - public const GENERAL_SERVICE_DISABLED = 'general_service_disabled'; - public const GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope'; - public const GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded'; - public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled'; - public const GENERAL_PHONE_DISABLED = 'general_phone_disabled'; - public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid'; - public const GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded'; - public const GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded'; - public const GENERAL_QUERY_INVALID = 'general_query_invalid'; - public const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; - public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found'; - public const GENERAL_SERVER_ERROR = 'general_server_error'; - public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; - public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; - public const GENERAL_USAGE_DISABLED = 'general_usage_disabled'; - public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; - public const GENERAL_INVALID_EMAIL = 'general_invalid_email'; - public const GENERAL_INVALID_PHONE = 'general_invalid_phone'; - public const GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied'; - public const GENERAL_BAD_REQUEST = 'general_bad_request'; + public const string GENERAL_UNKNOWN = 'general_unknown'; + public const string GENERAL_MOCK = 'general_mock'; + public const string GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden'; + public const string GENERAL_RESOURCE_BLOCKED = 'general_resource_blocked'; + public const string GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin'; + public const string GENERAL_API_DISABLED = 'general_api_disabled'; + public const string GENERAL_SERVICE_DISABLED = 'general_service_disabled'; + public const string GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope'; + public const string GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded'; + public const string GENERAL_SMTP_DISABLED = 'general_smtp_disabled'; + public const string GENERAL_PHONE_DISABLED = 'general_phone_disabled'; + public const string GENERAL_ARGUMENT_INVALID = 'general_argument_invalid'; + public const string GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded'; + public const string GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded'; + public const string GENERAL_QUERY_INVALID = 'general_query_invalid'; + public const string GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; + public const string GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found'; + public const string GENERAL_SERVER_ERROR = 'general_server_error'; + public const string GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; + public const string GENERAL_CODES_DISABLED = 'general_codes_disabled'; + public const string GENERAL_USAGE_DISABLED = 'general_usage_disabled'; + public const string GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; + public const string GENERAL_INVALID_EMAIL = 'general_invalid_email'; + public const string GENERAL_INVALID_PHONE = 'general_invalid_phone'; + public const string GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied'; + public const string GENERAL_BAD_REQUEST = 'general_bad_request'; /** Users */ - public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; - public const USER_CONSOLE_COUNT_EXCEEDED = 'user_console_count_exceeded'; - public const USER_JWT_INVALID = 'user_jwt_invalid'; - public const USER_ALREADY_EXISTS = 'user_already_exists'; - public const USER_BLOCKED = 'user_blocked'; - public const USER_INVALID_TOKEN = 'user_invalid_token'; - public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required'; - public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted'; - public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted'; - public const USER_INVALID_CODE = 'user_invalid_code'; - public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials'; - public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited'; - public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists'; - public const USER_NOT_FOUND = 'user_not_found'; - public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used'; - public const USER_PASSWORD_PERSONAL_DATA = 'password_personal_data'; - public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists'; - public const USER_PASSWORD_MISMATCH = 'user_password_mismatch'; - public const USER_SESSION_NOT_FOUND = 'user_session_not_found'; - public const USER_IDENTITY_NOT_FOUND = 'user_identity_not_found'; - public const USER_UNAUTHORIZED = 'user_unauthorized'; - public const USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported'; - public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists'; - public const USER_PHONE_NOT_FOUND = 'user_phone_not_found'; - public const USER_PHONE_NOT_VERIFIED = 'user_phone_not_verified'; - public const USER_EMAIL_NOT_FOUND = 'user_email_not_found'; - public const USER_EMAIL_NOT_VERIFIED = 'user_email_not_verified'; - public const USER_MISSING_ID = 'user_missing_id'; - public const USER_MORE_FACTORS_REQUIRED = 'user_more_factors_required'; - public const USER_INVALID_CHALLENGE = 'user_invalid_challenge'; - public const USER_AUTHENTICATOR_NOT_FOUND = 'user_authenticator_not_found'; - public const USER_AUTHENTICATOR_ALREADY_VERIFIED = 'user_authenticator_already_verified'; - public const USER_RECOVERY_CODES_ALREADY_EXISTS = 'user_recovery_codes_already_exists'; - public const USER_RECOVERY_CODES_NOT_FOUND = 'user_recovery_codes_not_found'; - public const USER_CHALLENGE_REQUIRED = 'user_challenge_required'; - public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request'; - public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized'; - public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; - public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_already_verified'; - public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified'; - public const USER_DELETION_PROHIBITED = 'user_deletion_prohibited'; - public const USER_TARGET_NOT_FOUND = 'user_target_not_found'; - public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists'; - public const USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set'; + public const string USER_COUNT_EXCEEDED = 'user_count_exceeded'; + public const string USER_CONSOLE_COUNT_EXCEEDED = 'user_console_count_exceeded'; + public const string USER_JWT_INVALID = 'user_jwt_invalid'; + public const string USER_ALREADY_EXISTS = 'user_already_exists'; + public const string USER_BLOCKED = 'user_blocked'; + public const string USER_INVALID_TOKEN = 'user_invalid_token'; + public const string USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required'; + public const string USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted'; + public const string USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted'; + public const string USER_INVALID_CODE = 'user_invalid_code'; + public const string USER_INVALID_CREDENTIALS = 'user_invalid_credentials'; + public const string USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited'; + public const string USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists'; + public const string USER_NOT_FOUND = 'user_not_found'; + public const string USER_PASSWORD_RECENTLY_USED = 'password_recently_used'; + public const string USER_PASSWORD_PERSONAL_DATA = 'password_personal_data'; + public const string USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists'; + public const string USER_PASSWORD_MISMATCH = 'user_password_mismatch'; + public const string USER_SESSION_NOT_FOUND = 'user_session_not_found'; + public const string USER_IDENTITY_NOT_FOUND = 'user_identity_not_found'; + public const string USER_UNAUTHORIZED = 'user_unauthorized'; + public const string USER_AUTH_METHOD_UNSUPPORTED = 'user_auth_method_unsupported'; + public const string USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists'; + public const string USER_PHONE_NOT_FOUND = 'user_phone_not_found'; + public const string USER_PHONE_NOT_VERIFIED = 'user_phone_not_verified'; + public const string USER_EMAIL_NOT_FOUND = 'user_email_not_found'; + public const string USER_EMAIL_NOT_VERIFIED = 'user_email_not_verified'; + public const string USER_MISSING_ID = 'user_missing_id'; + public const string USER_MORE_FACTORS_REQUIRED = 'user_more_factors_required'; + public const string USER_INVALID_CHALLENGE = 'user_invalid_challenge'; + public const string USER_AUTHENTICATOR_NOT_FOUND = 'user_authenticator_not_found'; + public const string USER_AUTHENTICATOR_ALREADY_VERIFIED = 'user_authenticator_already_verified'; + public const string USER_RECOVERY_CODES_ALREADY_EXISTS = 'user_recovery_codes_already_exists'; + public const string USER_RECOVERY_CODES_NOT_FOUND = 'user_recovery_codes_not_found'; + public const string USER_CHALLENGE_REQUIRED = 'user_challenge_required'; + public const string USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request'; + public const string USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized'; + public const string USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; + public const string USER_EMAIL_ALREADY_VERIFIED = 'user_email_already_verified'; + public const string USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified'; + public const string USER_DELETION_PROHIBITED = 'user_deletion_prohibited'; + public const string USER_TARGET_NOT_FOUND = 'user_target_not_found'; + public const string USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists'; + public const string USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set'; - public const API_KEY_EXPIRED = 'api_key_expired'; + public const string API_KEY_EXPIRED = 'api_key_expired'; /** Teams */ - public const TEAM_NOT_FOUND = 'team_not_found'; - public const TEAM_INVITE_NOT_FOUND = 'team_invite_not_found'; - public const TEAM_INVALID_SECRET = 'team_invalid_secret'; - public const TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch'; - public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch'; - public const TEAM_ALREADY_EXISTS = 'team_already_exists'; + public const string TEAM_NOT_FOUND = 'team_not_found'; + public const string TEAM_INVITE_NOT_FOUND = 'team_invite_not_found'; + public const string TEAM_INVALID_SECRET = 'team_invalid_secret'; + public const string TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch'; + public const string TEAM_INVITE_MISMATCH = 'team_invite_mismatch'; + public const string TEAM_ALREADY_EXISTS = 'team_already_exists'; /** Console */ - public const RESOURCE_ALREADY_EXISTS = 'resource_already_exists'; + public const string RESOURCE_ALREADY_EXISTS = 'resource_already_exists'; /** Membership */ - public const MEMBERSHIP_NOT_FOUND = 'membership_not_found'; - public const MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed'; - public const MEMBERSHIP_DELETION_PROHIBITED = 'membership_deletion_prohibited'; - public const MEMBERSHIP_DOWNGRADE_PROHIBITED = 'membership_downgrade_prohibited'; + public const string MEMBERSHIP_NOT_FOUND = 'membership_not_found'; + public const string MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed'; + public const string MEMBERSHIP_DELETION_PROHIBITED = 'membership_deletion_prohibited'; + public const string MEMBERSHIP_DOWNGRADE_PROHIBITED = 'membership_downgrade_prohibited'; /** Avatars */ - public const AVATAR_SET_NOT_FOUND = 'avatar_set_not_found'; - public const AVATAR_NOT_FOUND = 'avatar_not_found'; - public const AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found'; - public const AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed'; - public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found'; - public const AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed'; + public const string AVATAR_SET_NOT_FOUND = 'avatar_set_not_found'; + public const string AVATAR_NOT_FOUND = 'avatar_not_found'; + public const string AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found'; + public const string AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed'; + public const string AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found'; + public const string AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed'; /** Storage */ - public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists'; - public const STORAGE_FILE_NOT_FOUND = 'storage_file_not_found'; - public const STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found'; - public const STORAGE_FILE_EMPTY = 'storage_file_empty'; - public const STORAGE_FILE_TYPE_UNSUPPORTED = 'storage_file_type_unsupported'; - public const STORAGE_INVALID_FILE_SIZE = 'storage_invalid_file_size'; - public const STORAGE_INVALID_FILE = 'storage_invalid_file'; - public const STORAGE_BUCKET_ALREADY_EXISTS = 'storage_bucket_already_exists'; - public const STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found'; - public const STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range'; - public const STORAGE_INVALID_RANGE = 'storage_invalid_range'; - public const STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id'; - public const STORAGE_FILE_NOT_PUBLIC = 'storage_file_not_public'; + public const string STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists'; + public const string STORAGE_FILE_NOT_FOUND = 'storage_file_not_found'; + public const string STORAGE_DEVICE_NOT_FOUND = 'storage_device_not_found'; + public const string STORAGE_FILE_EMPTY = 'storage_file_empty'; + public const string STORAGE_FILE_TYPE_UNSUPPORTED = 'storage_file_type_unsupported'; + public const string STORAGE_INVALID_FILE_SIZE = 'storage_invalid_file_size'; + public const string STORAGE_INVALID_FILE = 'storage_invalid_file'; + public const string STORAGE_BUCKET_ALREADY_EXISTS = 'storage_bucket_already_exists'; + public const string STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found'; + public const string STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range'; + public const string STORAGE_INVALID_RANGE = 'storage_invalid_range'; + public const string STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id'; + public const string STORAGE_FILE_NOT_PUBLIC = 'storage_file_not_public'; /** VCS */ - public const INSTALLATION_NOT_FOUND = 'installation_not_found'; - public const PROVIDER_REPOSITORY_NOT_FOUND = 'provider_repository_not_found'; - public const REPOSITORY_NOT_FOUND = 'repository_not_found'; - public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict'; - public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure'; + public const string INSTALLATION_NOT_FOUND = 'installation_not_found'; + public const string PROVIDER_REPOSITORY_NOT_FOUND = 'provider_repository_not_found'; + public const string REPOSITORY_NOT_FOUND = 'repository_not_found'; + public const string PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict'; + public const string GENERAL_PROVIDER_FAILURE = 'general_provider_failure'; /** Sites */ - public const SITE_NOT_FOUND = 'site_not_found'; - public const SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found'; + public const string SITE_NOT_FOUND = 'site_not_found'; + public const string SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found'; /** Functions */ - public const FUNCTION_NOT_FOUND = 'function_not_found'; - public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; - public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing'; - public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; - public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; - public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; - public const FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing'; + public const string FUNCTION_NOT_FOUND = 'function_not_found'; + public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; + public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing'; + public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; + public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; + public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; + public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing'; /** Deployments */ - public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found'; + public const string DEPLOYMENT_NOT_FOUND = 'deployment_not_found'; /** Builds */ - public const BUILD_NOT_FOUND = 'build_not_found'; - public const BUILD_NOT_READY = 'build_not_ready'; - public const BUILD_IN_PROGRESS = 'build_in_progress'; - public const BUILD_ALREADY_COMPLETED = 'build_already_completed'; - public const BUILD_CANCELED = 'build_canceled'; - public const BUILD_FAILED = 'build_failed'; + public const string BUILD_NOT_FOUND = 'build_not_found'; + public const string BUILD_NOT_READY = 'build_not_ready'; + public const string BUILD_IN_PROGRESS = 'build_in_progress'; + public const string BUILD_ALREADY_COMPLETED = 'build_already_completed'; + public const string BUILD_CANCELED = 'build_canceled'; + public const string BUILD_FAILED = 'build_failed'; /** Execution */ - public const EXECUTION_NOT_FOUND = 'execution_not_found'; - public const EXECUTION_IN_PROGRESS = 'execution_in_progress'; + public const string EXECUTION_NOT_FOUND = 'execution_not_found'; + public const string EXECUTION_IN_PROGRESS = 'execution_in_progress'; /** Log */ - public const LOG_NOT_FOUND = 'log_not_found'; + public const string LOG_NOT_FOUND = 'log_not_found'; /** Databases */ - public const DATABASE_NOT_FOUND = 'database_not_found'; - public const DATABASE_ALREADY_EXISTS = 'database_already_exists'; - public const DATABASE_TIMEOUT = 'database_timeout'; - public const DATABASE_QUERY_ORDER_NULL = 'database_query_order_null'; + public const string DATABASE_NOT_FOUND = 'database_not_found'; + public const string DATABASE_ALREADY_EXISTS = 'database_already_exists'; + public const string DATABASE_TIMEOUT = 'database_timeout'; + public const string DATABASE_QUERY_ORDER_NULL = 'database_query_order_null'; /** Collections */ - public const COLLECTION_NOT_FOUND = 'collection_not_found'; - public const COLLECTION_ALREADY_EXISTS = 'collection_already_exists'; - public const COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded'; + public const string COLLECTION_NOT_FOUND = 'collection_not_found'; + public const string COLLECTION_ALREADY_EXISTS = 'collection_already_exists'; + public const string COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded'; /** Tables */ - public const TABLE_NOT_FOUND = 'table_not_found'; - public const TABLE_ALREADY_EXISTS = 'table_already_exists'; - public const TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded'; + public const string TABLE_NOT_FOUND = 'table_not_found'; + public const string TABLE_ALREADY_EXISTS = 'table_already_exists'; + public const string TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded'; /** Documents */ - public const DOCUMENT_NOT_FOUND = 'document_not_found'; - public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure'; - public const DOCUMENT_MISSING_DATA = 'document_missing_data'; - public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload'; - public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists'; - public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict'; - public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted'; + public const string DOCUMENT_NOT_FOUND = 'document_not_found'; + public const string DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure'; + public const string DOCUMENT_MISSING_DATA = 'document_missing_data'; + public const string DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload'; + public const string DOCUMENT_ALREADY_EXISTS = 'document_already_exists'; + public const string DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict'; + public const string DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted'; /** Rows */ - public const ROW_NOT_FOUND = 'row_not_found'; - public const ROW_INVALID_STRUCTURE = 'row_invalid_structure'; - public const ROW_MISSING_DATA = 'row_missing_data'; - public const ROW_MISSING_PAYLOAD = 'row_missing_payload'; - public const ROW_ALREADY_EXISTS = 'row_already_exists'; - public const ROW_UPDATE_CONFLICT = 'row_update_conflict'; - public const ROW_DELETE_RESTRICTED = 'row_delete_restricted'; + public const string ROW_NOT_FOUND = 'row_not_found'; + public const string ROW_INVALID_STRUCTURE = 'row_invalid_structure'; + public const string ROW_MISSING_DATA = 'row_missing_data'; + public const string ROW_MISSING_PAYLOAD = 'row_missing_payload'; + public const string ROW_ALREADY_EXISTS = 'row_already_exists'; + public const string ROW_UPDATE_CONFLICT = 'row_update_conflict'; + public const string ROW_DELETE_RESTRICTED = 'row_delete_restricted'; /** Attributes */ - public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found'; - public const ATTRIBUTE_UNKNOWN = 'attribute_unknown'; - public const ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available'; - public const ATTRIBUTE_FORMAT_UNSUPPORTED = 'attribute_format_unsupported'; - public const ATTRIBUTE_DEFAULT_UNSUPPORTED = 'attribute_default_unsupported'; - public const ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists'; - public const ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded'; - public const ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid'; - public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid'; - public const ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize'; + public const string ATTRIBUTE_NOT_FOUND = 'attribute_not_found'; + public const string ATTRIBUTE_UNKNOWN = 'attribute_unknown'; + public const string ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available'; + public const string ATTRIBUTE_FORMAT_UNSUPPORTED = 'attribute_format_unsupported'; + public const string ATTRIBUTE_DEFAULT_UNSUPPORTED = 'attribute_default_unsupported'; + public const string ATTRIBUTE_ALREADY_EXISTS = 'attribute_already_exists'; + public const string ATTRIBUTE_LIMIT_EXCEEDED = 'attribute_limit_exceeded'; + public const string ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid'; + public const string ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid'; + public const string ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize'; /** Columns */ - public const COLUMN_NOT_FOUND = 'column_not_found'; - public const COLUMN_UNKNOWN = 'column_unknown'; - public const COLUMN_NOT_AVAILABLE = 'column_not_available'; - public const COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported'; - public const COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported'; - public const COLUMN_ALREADY_EXISTS = 'column_already_exists'; - public const COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded'; - public const COLUMN_VALUE_INVALID = 'column_value_invalid'; - public const COLUMN_TYPE_INVALID = 'column_type_invalid'; - public const COLUMN_INVALID_RESIZE = 'column_invalid_resize'; + public const string COLUMN_NOT_FOUND = 'column_not_found'; + public const string COLUMN_UNKNOWN = 'column_unknown'; + public const string COLUMN_NOT_AVAILABLE = 'column_not_available'; + public const string COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported'; + public const string COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported'; + public const string COLUMN_ALREADY_EXISTS = 'column_already_exists'; + public const string COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded'; + public const string COLUMN_VALUE_INVALID = 'column_value_invalid'; + public const string COLUMN_TYPE_INVALID = 'column_type_invalid'; + public const string COLUMN_INVALID_RESIZE = 'column_invalid_resize'; /** Relationship */ - public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid'; + public const string RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid'; /** Indexes */ - public const INDEX_NOT_FOUND = 'index_not_found'; - public const INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded'; - public const INDEX_ALREADY_EXISTS = 'index_already_exists'; - public const INDEX_INVALID = 'index_invalid'; - public const INDEX_DEPENDENCY = 'index_dependency'; + public const string INDEX_NOT_FOUND = 'index_not_found'; + public const string INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded'; + public const string INDEX_ALREADY_EXISTS = 'index_already_exists'; + public const string INDEX_INVALID = 'index_invalid'; + public const string INDEX_DEPENDENCY = 'index_dependency'; /** Column Indexes */ - public const COLUMN_INDEX_NOT_FOUND = 'column_index_not_found'; - public const COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded'; - public const COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists'; - public const COLUMN_INDEX_INVALID = 'column_index_invalid'; - public const COLUMN_INDEX_DEPENDENCY = 'column_index_dependency'; + public const string COLUMN_INDEX_NOT_FOUND = 'column_index_not_found'; + public const string COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded'; + public const string COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists'; + public const string COLUMN_INDEX_INVALID = 'column_index_invalid'; + public const string COLUMN_INDEX_DEPENDENCY = 'column_index_dependency'; /** Transactions */ - public const TRANSACTION_NOT_FOUND = 'transaction_not_found'; - public const TRANSACTION_ALREADY_EXISTS = 'transaction_already_exists'; - public const TRANSACTION_INVALID = 'transaction_invalid'; - public const TRANSACTION_FAILED = 'transaction_expired'; - public const TRANSACTION_EXPIRED = 'transaction_expired'; - public const TRANSACTION_CONFLICT = 'transaction_conflict'; - public const TRANSACTION_LIMIT_EXCEEDED = 'transaction_limit_exceeded'; - public const TRANSACTION_NOT_READY = 'transaction_not_ready'; + public const string TRANSACTION_NOT_FOUND = 'transaction_not_found'; + public const string TRANSACTION_ALREADY_EXISTS = 'transaction_already_exists'; + public const string TRANSACTION_INVALID = 'transaction_invalid'; + public const string TRANSACTION_FAILED = 'transaction_failed'; + public const string TRANSACTION_EXPIRED = 'transaction_expired'; + public const string TRANSACTION_CONFLICT = 'transaction_conflict'; + public const string TRANSACTION_LIMIT_EXCEEDED = 'transaction_limit_exceeded'; + public const string TRANSACTION_NOT_READY = 'transaction_not_ready'; /** Projects */ - public const PROJECT_NOT_FOUND = 'project_not_found'; - public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled'; - public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported'; - public const PROJECT_ALREADY_EXISTS = 'project_already_exists'; - public const PROJECT_INVALID_SUCCESS_URL = 'project_invalid_success_url'; - public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url'; - public const PROJECT_RESERVED_PROJECT = 'project_reserved_project'; - public const PROJECT_KEY_EXPIRED = 'project_key_expired'; + public const string PROJECT_NOT_FOUND = 'project_not_found'; + public const string PROJECT_PROVIDER_DISABLED = 'project_provider_disabled'; + public const string PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported'; + public const string PROJECT_ALREADY_EXISTS = 'project_already_exists'; + public const string PROJECT_INVALID_SUCCESS_URL = 'project_invalid_success_url'; + public const string PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url'; + public const string PROJECT_RESERVED_PROJECT = 'project_reserved_project'; + public const string PROJECT_KEY_EXPIRED = 'project_key_expired'; - public const PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid'; + public const string PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid'; - public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion'; + public const string PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion'; - public const PROJECT_REGION_UNSUPPORTED = 'project_region_unsupported'; + public const string PROJECT_REGION_UNSUPPORTED = 'project_region_unsupported'; /** Webhooks */ - public const WEBHOOK_NOT_FOUND = 'webhook_not_found'; + public const string WEBHOOK_NOT_FOUND = 'webhook_not_found'; /** Router */ - public const ROUTER_HOST_NOT_FOUND = 'router_host_not_found'; - public const ROUTER_DOMAIN_NOT_CONFIGURED = 'router_domain_not_configured'; + public const string ROUTER_HOST_NOT_FOUND = 'router_host_not_found'; + public const string ROUTER_DOMAIN_NOT_CONFIGURED = 'router_domain_not_configured'; /** Proxy */ - public const RULE_RESOURCE_NOT_FOUND = 'rule_resource_not_found'; - public const RULE_NOT_FOUND = 'rule_not_found'; - public const RULE_ALREADY_EXISTS = 'rule_already_exists'; - public const RULE_VERIFICATION_FAILED = 'rule_verification_failed'; + public const string RULE_RESOURCE_NOT_FOUND = 'rule_resource_not_found'; + public const string RULE_NOT_FOUND = 'rule_not_found'; + public const string RULE_ALREADY_EXISTS = 'rule_already_exists'; + public const string RULE_VERIFICATION_FAILED = 'rule_verification_failed'; /** Keys */ - public const KEY_NOT_FOUND = 'key_not_found'; + public const string KEY_NOT_FOUND = 'key_not_found'; /** Variables */ - public const VARIABLE_NOT_FOUND = 'variable_not_found'; - public const VARIABLE_ALREADY_EXISTS = 'variable_already_exists'; - public const VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret'; + public const string VARIABLE_NOT_FOUND = 'variable_not_found'; + public const string VARIABLE_ALREADY_EXISTS = 'variable_already_exists'; + public const string VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret'; /** Platform */ - public const PLATFORM_NOT_FOUND = 'platform_not_found'; + public const string PLATFORM_NOT_FOUND = 'platform_not_found'; /** GraphqQL */ - public const GRAPHQL_NO_QUERY = 'graphql_no_query'; - public const GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries'; + public const string GRAPHQL_NO_QUERY = 'graphql_no_query'; + public const string GRAPHQL_TOO_MANY_QUERIES = 'graphql_too_many_queries'; /** Migrations */ - public const MIGRATION_NOT_FOUND = 'migration_not_found'; - public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists'; - public const MIGRATION_IN_PROGRESS = 'migration_in_progress'; - public const MIGRATION_PROVIDER_ERROR = 'migration_provider_error'; + public const string MIGRATION_NOT_FOUND = 'migration_not_found'; + public const string MIGRATION_ALREADY_EXISTS = 'migration_already_exists'; + public const string MIGRATION_IN_PROGRESS = 'migration_in_progress'; + public const string MIGRATION_PROVIDER_ERROR = 'migration_provider_error'; /** Realtime */ - public const REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid'; - public const REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages'; - public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation'; + public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid'; + public const string REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages'; + public const string REALTIME_POLICY_VIOLATION = 'realtime_policy_violation'; /** Health */ - public const HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded'; - public const HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired'; - public const HEALTH_INVALID_HOST = 'health_invalid_host'; + public const string HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded'; + public const string HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired'; + public const string HEALTH_INVALID_HOST = 'health_invalid_host'; /** Provider */ - public const PROVIDER_NOT_FOUND = 'provider_not_found'; - public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists'; - public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type'; - public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials'; + public const string PROVIDER_NOT_FOUND = 'provider_not_found'; + public const string PROVIDER_ALREADY_EXISTS = 'provider_already_exists'; + public const string PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type'; + public const string PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials'; /** Topic */ - public const TOPIC_NOT_FOUND = 'topic_not_found'; - public const TOPIC_ALREADY_EXISTS = 'topic_already_exists'; + public const string TOPIC_NOT_FOUND = 'topic_not_found'; + public const string TOPIC_ALREADY_EXISTS = 'topic_already_exists'; /** Subscriber */ - public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found'; - public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists'; + public const string SUBSCRIBER_NOT_FOUND = 'subscriber_not_found'; + public const string SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists'; /** Message */ - public const MESSAGE_NOT_FOUND = 'message_not_found'; - public const MESSAGE_MISSING_TARGET = 'message_missing_target'; - public const MESSAGE_ALREADY_SENT = 'message_already_sent'; - public const MESSAGE_ALREADY_PROCESSING = 'message_already_processing'; - public const MESSAGE_ALREADY_FAILED = 'message_already_failed'; - public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled'; - public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email'; - public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms'; - public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push'; - public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule'; + public const string MESSAGE_NOT_FOUND = 'message_not_found'; + public const string MESSAGE_MISSING_TARGET = 'message_missing_target'; + public const string MESSAGE_ALREADY_SENT = 'message_already_sent'; + public const string MESSAGE_ALREADY_PROCESSING = 'message_already_processing'; + public const string MESSAGE_ALREADY_FAILED = 'message_already_failed'; + public const string MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled'; + public const string MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email'; + public const string MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms'; + public const string MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push'; + public const string MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule'; /** Targets */ - public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type'; + public const string TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type'; /** Schedules */ - public const SCHEDULE_NOT_FOUND = 'schedule_not_found'; + public const string SCHEDULE_NOT_FOUND = 'schedule_not_found'; /** Tokens */ - public const TOKEN_NOT_FOUND = 'token_not_found'; - public const TOKEN_EXPIRED = 'token_expired'; - public const TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid'; + public const string TOKEN_NOT_FOUND = 'token_not_found'; + public const string TOKEN_EXPIRED = 'token_expired'; + public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid'; protected string $type = ''; protected array $errors = []; @@ -378,7 +378,13 @@ class Exception extends \Exception private array $ctas = []; private ?string $view = null; - public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, ?string $view = null) + public function __construct( + string $type = Exception::GENERAL_UNKNOWN, + string $message = null, + int|string $code = null, + \Throwable $previous = null, + ?string $view = null + ) { $this->errors = Config::getParam('errors'); $this->type = $type; @@ -388,7 +394,7 @@ class Exception extends \Exception // Mark string errors like HY001 from PDO as 500 errors if (\is_string($this->code)) { if (\is_numeric($this->code)) { - $this->code = (int) $this->code; + $this->code = (int)$this->code; } else { $this->code = 500; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 58ec470bd2..63f34241bd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -270,6 +270,12 @@ class Update extends Action ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); + } catch (NotFoundException $e) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + + throw new Exception(Exception::DOCUMENT_NOT_FOUND); } catch (DuplicateException|ConflictException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', From de5df4b72ec27a6113c2512dacc318d2d6bbb9f2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Sep 2025 03:57:03 +1200 Subject: [PATCH 071/145] Add extra tests --- app/config/errors.php | 2 +- composer.lock | 46 +- src/Appwrite/Extend/Exception.php | 3 +- .../Databases/Http/Transactions/Update.php | 25 +- .../{ACIDComplianceTest.php => ACIDTest.php} | 10 +- .../DatabasesTransactionsTest.php | 375 ----- .../Transactions/TransactionsTest.php | 1385 +++++++++++++++++ 7 files changed, 1427 insertions(+), 419 deletions(-) rename tests/e2e/Services/Databases/Transactions/{ACIDComplianceTest.php => ACIDTest.php} (99%) delete mode 100644 tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php create mode 100644 tests/e2e/Services/Databases/Transactions/TransactionsTest.php diff --git a/app/config/errors.php b/app/config/errors.php index 098c87c574..e76ca9ff68 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -989,7 +989,7 @@ return [ Exception::TRANSACTION_LIMIT_EXCEEDED => [ 'name' => Exception::TRANSACTION_LIMIT_EXCEEDED, 'description' => 'The maximum number of operations per transaction has been exceeded.', - 'code' => 422, + 'code' => 400, ], Exception::TRANSACTION_NOT_READY => [ 'name' => Exception::TRANSACTION_NOT_READY, diff --git a/composer.lock b/composer.lock index 90bb2f12f6..ed66af8572 100644 --- a/composer.lock +++ b/composer.lock @@ -1515,16 +1515,16 @@ }, { "name": "open-telemetry/sem-conv", - "version": "1.36.0", + "version": "1.37.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "60dd18fd21d45e6f4234ecab89c14021b6e3de9a" + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/60dd18fd21d45e6f4234ecab89c14021b6e3de9a", - "reference": "60dd18fd21d45e6f4234ecab89c14021b6e3de9a", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1", + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1", "shasum": "" }, "require": { @@ -1568,7 +1568,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-04T03:22:08+00:00" + "time": "2025-09-03T12:08:10+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.2.4", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "87fb55e86892eecd726635eb1829acb743c2c156" + "reference": "fcd166b715a14cfea11f7a9c47d4c0076bedcecd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/87fb55e86892eecd726635eb1829acb743c2c156", - "reference": "87fb55e86892eecd726635eb1829acb743c2c156", + "url": "https://api.github.com/repos/utopia-php/database/zipball/fcd166b715a14cfea11f7a9c47d4c0076bedcecd", + "reference": "fcd166b715a14cfea11f7a9c47d4c0076bedcecd", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.2.4" + "source": "https://github.com/utopia-php/database/tree/1.3.1" }, - "time": "2025-09-01T06:01:09+00:00" + "time": "2025-09-03T15:50:41+00:00" }, { "name": "utopia-php/detector", @@ -3942,16 +3942,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.22", + "version": "0.33.23", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "c01a815cb976c9255e045fc3bcc3f5fcf477e0bc" + "reference": "88e8002365c10a727014ecc56322bcd1d780ceed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/c01a815cb976c9255e045fc3bcc3f5fcf477e0bc", - "reference": "c01a815cb976c9255e045fc3bcc3f5fcf477e0bc", + "url": "https://api.github.com/repos/utopia-php/http/zipball/88e8002365c10a727014ecc56322bcd1d780ceed", + "reference": "88e8002365c10a727014ecc56322bcd1d780ceed", "shasum": "" }, "require": { @@ -3983,9 +3983,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.22" + "source": "https://github.com/utopia-php/http/tree/0.33.23" }, - "time": "2025-08-26T10:29:50+00:00" + "time": "2025-09-03T11:58:14+00:00" }, { "name": "utopia-php/image", @@ -5007,16 +5007,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.1.15", + "version": "1.1.16", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "8e8e39634ba7558704522959d88f3542563a5444" + "reference": "f8fbc4b1ba0e918825338f50cbdea4d887389c41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8e8e39634ba7558704522959d88f3542563a5444", - "reference": "8e8e39634ba7558704522959d88f3542563a5444", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f8fbc4b1ba0e918825338f50cbdea4d887389c41", + "reference": "f8fbc4b1ba0e918825338f50cbdea4d887389c41", "shasum": "" }, "require": { @@ -5052,9 +5052,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.1.15" + "source": "https://github.com/appwrite/sdk-generator/tree/1.1.16" }, - "time": "2025-08-27T04:59:35+00:00" + "time": "2025-09-03T06:50:04+00:00" }, { "name": "doctrine/annotations", diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index cfa55c540e..2047cfc044 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -384,8 +384,7 @@ class Exception extends \Exception int|string $code = null, \Throwable $previous = null, ?string $view = null - ) - { + ) { $this->errors = Config::getParam('errors'); $this->type = $type; $this->view = $view; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 63f34241bd..9d4eae41c9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; use Utopia\Database\Validator\UID; @@ -167,12 +168,17 @@ class Update extends Action break; case 'update': - $document = new Document($data); - $state[$collectionId][$documentId] = $dbForProject->updateDocument( + $document = $dbForProject->updateDocument( $collectionId, $documentId, - $document, + new Document($data), ); + + if ($document->isEmpty()) { + throw new ConflictException(''); + } + + $state[$collectionId][$documentId] = $document; break; case 'upsert': @@ -258,11 +264,7 @@ class Update extends Action $transaction = $dbForProject->updateDocument( 'transactions', $transactionId, - new Document( - [ - 'status' => 'committed', - ] - ) + new Document(['status' => 'committed']) ); // Clear the transaction logs @@ -270,23 +272,20 @@ class Update extends Action ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); - } catch (NotFoundException $e) { + } catch (NotFoundException) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); - throw new Exception(Exception::DOCUMENT_NOT_FOUND); - } catch (DuplicateException|ConflictException $e) { + } catch (DuplicateException|ConflictException) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); - throw new Exception(Exception::TRANSACTION_CONFLICT); } catch (TransactionException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); - throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } }); diff --git a/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php b/tests/e2e/Services/Databases/Transactions/ACIDTest.php similarity index 99% rename from tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php rename to tests/e2e/Services/Databases/Transactions/ACIDTest.php index 42a2967685..7bf027b828 100644 --- a/tests/e2e/Services/Databases/Transactions/ACIDComplianceTest.php +++ b/tests/e2e/Services/Databases/Transactions/ACIDTest.php @@ -12,7 +12,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -class ACIDComplianceTest extends Scope +class ACIDTest extends Scope { use ProjectCustom; use SideClient; @@ -20,7 +20,7 @@ class ACIDComplianceTest extends Scope /** * Test atomicity - all operations succeed or all fail */ - public function testTransactionAtomicity(): void + public function testAtomicity(): void { // Create database $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -175,7 +175,7 @@ class ACIDComplianceTest extends Scope /** * Test consistency - schema validation and constraints */ - public function testTransactionConsistency(): void + public function testConsistency(): void { // Create database $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -306,7 +306,7 @@ class ACIDComplianceTest extends Scope /** * Test isolation - concurrent transactions on same data */ - public function testTransactionIsolation(): void + public function testIsolation(): void { // Create database $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -463,7 +463,7 @@ class ACIDComplianceTest extends Scope /** * Test durability - committed data persists */ - public function testTransactionDurability(): void + public function testDurability(): void { // Create database $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ diff --git a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php b/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php deleted file mode 100644 index 513051fcc0..0000000000 --- a/tests/e2e/Services/Databases/Transactions/DatabasesTransactionsTest.php +++ /dev/null @@ -1,375 +0,0 @@ -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' => 'TransactionTestDatabase' - ]); - - $this->assertEquals(201, $database['headers']['status-code']); - $databaseId = $database['body']['$id']; - - // Test creating a transaction with default TTL - $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertArrayHasKey('$id', $response['body']); - $this->assertArrayHasKey('status', $response['body']); - $this->assertArrayHasKey('operations', $response['body']); - $this->assertArrayHasKey('expiresAt', $response['body']); - $this->assertEquals('pending', $response['body']['status']); - $this->assertEquals(0, $response['body']['operations']); - - $transactionId1 = $response['body']['$id']; - - // Test creating a transaction with custom TTL - $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'ttl' => 900 - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertEquals('pending', $response['body']['status']); - - $expiresAt = new \DateTime($response['body']['expiresAt']); - $now = new \DateTime(); - $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); - $this->assertGreaterThan(800, $diff); - $this->assertLessThan(1000, $diff); - - $transactionId2 = $response['body']['$id']; - - // Test invalid TTL values - $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'ttl' => 30 // Below minimum - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'ttl' => 4000 // Above maximum - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - return [ - 'databaseId' => $databaseId, - 'transactionId1' => $transactionId1, - 'transactionId2' => $transactionId2 - ]; - } - - /** - * @depends testCreate - */ - public function testAddOperations(array $data): array - { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId1']; - - // Create a collection for testing - $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' => 'TransactionOperationsTest', - 'documentSecurity' => false, - 'permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $this->assertEquals(201, $collection['headers']['status-code']); - $collectionId = $collection['body']['$id']; - - // Add attributes - $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'name', - 'size' => 256, - 'required' => true, - ]); - - $this->assertEquals(202, $attribute['headers']['status-code']); - - // Wait for attribute to be created - sleep(2); - - // Add valid operations - $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'operations' => [ - [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'action' => 'create', - 'documentId' => 'doc1', - 'data' => [ - 'name' => 'Test Document 1' - ] - ], - [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'action' => 'create', - 'documentId' => 'doc2', - 'data' => [ - 'name' => 'Test Document 2' - ] - ] - ] - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertEquals(2, $response['body']['operations']); - - // Test adding more operations - $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'operations' => [ - [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'action' => 'update', - 'documentId' => 'doc1', - 'data' => [ - 'name' => 'Updated Document 1' - ] - ] - ] - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - $this->assertEquals(3, $response['body']['operations']); - - // Test invalid database ID - $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'operations' => [ - [ - 'databaseId' => 'invalid_database', - 'collectionId' => $collectionId, - 'action' => 'create', - 'documentId' => ID::unique(), - 'data' => ['name' => 'Test'] - ] - ] - ]); - - $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); - - // Test invalid collection ID - $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'operations' => [ - [ - 'databaseId' => $databaseId, - 'collectionId' => 'invalid_collection', - 'action' => 'create', - 'documentId' => ID::unique(), - 'data' => ['name' => 'Test'] - ] - ] - ]); - - $this->assertEquals(404, $response['headers']['status-code']); - - return array_merge($data, [ - 'collectionId' => $collectionId - ]); - } - - /** - * @depends testAddOperations - */ - public function testCommit(array $data): void - { - $databaseId = $data['databaseId']; - $collectionId = $data['collectionId']; - $transactionId = $data['transactionId1']; - - // Commit the transaction - $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('committed', $response['body']['status']); - - // Verify documents were created - $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $documents['headers']['status-code']); - $this->assertEquals(2, $documents['body']['total']); - - // Verify the update was applied - $doc1Found = false; - foreach ($documents['body']['documents'] as $doc) { - if ($doc['$id'] === 'doc1') { - $this->assertEquals('Updated Document 1', $doc['name']); - $doc1Found = true; - } - } - $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); - - // Test committing already committed transaction - $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - } - - /** - * @depends testCreate - */ - public function testRollback(array $data): void - { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId2']; - - // Create a collection for rollback test - $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' => 'TransactionRollbackTest', - 'documentSecurity' => false, - 'permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - - $collectionId = $collection['body']['$id']; - - // Add attribute - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'value', - 'size' => 256, - 'required' => true, - ]); - - sleep(2); - - // Add operations - $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'operations' => [ - [ - 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'action' => 'create', - 'documentId' => 'rollback_doc', - 'data' => [ - 'value' => 'Should not exist' - ] - ] - ] - ]); - - $this->assertEquals(201, $response['headers']['status-code']); - - // Rollback the transaction - $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'rollback' => true - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('rolledBack', $response['body']['status']); - - // Verify no documents were created - $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $documents['headers']['status-code']); - $this->assertEquals(0, $documents['body']['total']); - } -} diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php new file mode 100644 index 0000000000..cb0f3f3827 --- /dev/null +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -0,0 +1,1385 @@ +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' => 'TransactionTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Test creating a transaction with default TTL + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('status', $response['body']); + $this->assertArrayHasKey('operations', $response['body']); + $this->assertArrayHasKey('expiresAt', $response['body']); + $this->assertEquals('pending', $response['body']['status']); + $this->assertEquals(0, $response['body']['operations']); + + $transactionId1 = $response['body']['$id']; + + // Test creating a transaction with custom TTL + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 900 + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals('pending', $response['body']['status']); + + $expiresAt = new \DateTime($response['body']['expiresAt']); + $now = new \DateTime(); + $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThan(800, $diff); + $this->assertLessThan(1000, $diff); + + $transactionId2 = $response['body']['$id']; + + // Test invalid TTL values + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 30 // Below minimum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 4000 // Above maximum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return [ + 'databaseId' => $databaseId, + 'transactionId1' => $transactionId1, + 'transactionId2' => $transactionId2 + ]; + } + + /** + * @depends testCreate + */ + public function testAddOperations(array $data): array + { + $databaseId = $data['databaseId']; + $transactionId = $data['transactionId1']; + + // Create a collection for testing + $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' => 'TransactionOperationsTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add attributes + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + + // Wait for attribute to be created + sleep(2); + + // Add valid operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + 'name' => 'Test Document 2' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['operations']); + + // Test adding more operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); + + // Test invalid database ID + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => 'invalid_database', + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); + + // Test invalid collection ID + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => 'invalid_collection', + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + return array_merge($data, [ + 'collectionId' => $collectionId + ]); + } + + /** + * @depends testAddOperations + */ + public function testCommit(array $data): void + { + $databaseId = $data['databaseId']; + $collectionId = $data['collectionId']; + $transactionId = $data['transactionId1']; + + // Commit the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify documents were created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(2, $documents['body']['total']); + + // Verify the update was applied + $doc1Found = false; + foreach ($documents['body']['documents'] as $doc) { + if ($doc['$id'] === 'doc1') { + $this->assertEquals('Updated Document 1', $doc['name']); + $doc1Found = true; + } + } + $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); + + // Test committing already committed transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * @depends testCreate + */ + public function testRollback(array $data): void + { + $databaseId = $data['databaseId']; + $transactionId = $data['transactionId2']; + + // Create a collection for rollback test + $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' => 'TransactionRollbackTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attribute + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'rollback_doc', + 'data' => [ + 'value' => 'Should not exist' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Rollback the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('rolledBack', $response['body']['status']); + + // Verify no documents were created + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(0, $documents['body']['total']); + } + + /** + * Test transaction expiration + */ + public function testTransactionExpiration(): 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' => 'ExpirationTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction with minimum TTL (60 seconds) + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 60 + ]); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Add operation + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['data' => 'Should expire'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Verify transaction was created with correct expiration + $txnDetails = $this->client->call(Client::METHOD_GET, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $txnDetails['headers']['status-code']); + $this->assertEquals('pending', $txnDetails['body']['status']); + + // Verify expiration time is approximately 60 seconds from now + $expiresAt = new \DateTime($txnDetails['body']['expiresAt']); + $now = new \DateTime(); + $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThan(55, $diff); + $this->assertLessThan(65, $diff); + } + + /** + * Test maximum operations per transaction + */ + public function testTransactionSizeLimit(): 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' => 'SizeLimitTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [Permission::create(Role::any())], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Try to add operations exceeding the limit (assuming limit is 100) + // We'll add 50 operations twice to test incremental limit + $operations = []; + for ($i = 0; $i < 50; $i++) { + $operations[] = [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc_' . $i, + 'data' => ['value' => 'Test ' . $i] + ]; + } + + // First batch should succeed + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => $operations + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(50, $response['body']['operations']); + + // Second batch of 50 more operations + $operations = []; + for ($i = 50; $i < 100; $i++) { + $operations[] = [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'doc_' . $i, + 'action' => 'create', + 'data' => ['value' => 'Test ' . $i] + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => $operations + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(100, $response['body']['operations']); + + // Try to add one more operation - should fail + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc_overflow', + 'data' => ['value' => 'This should fail'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test concurrent transactions with conflicting operations + */ + public function testConcurrentTransactionConflicts(): 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' => 'ConflictTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create 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' => 'counter', + 'required' => true, + 'min' => 0, + 'max' => 1000000, + ]); + + sleep(2); + + // Create initial document + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'shared_doc', + 'data' => ['counter' => 100] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create two transactions + $txn1 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $txn2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId1 = $txn1['body']['$id']; + $transactionId2 = $txn2['body']['$id']; + + // Both transactions try to update the same document + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId1}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'shared_doc', + 'data' => ['counter' => 200] + ] + ] + ]); + + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'shared_doc', + 'data' => ['counter' => 300] + ] + ] + ]); + + // Commit first transaction + $response1 = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId1}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response1['headers']['status-code']); + + // Commit second transaction - should fail with conflict + $response2 = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response2['headers']['status-code']); // Conflict + + // Verify the document has the value from first transaction + $doc = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['body']['counter']); + } + + /** + * Test deleting a document that's being updated in a transaction + */ + public function testDeleteDocumentDuringTransaction(): 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' => 'DeleteConflictDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create document + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'target_doc', + 'data' => ['data' => 'Original'] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add update operation to transaction + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'target_doc', + 'data' => ['data' => 'Updated in transaction'] + ] + ] + ]); + + // Delete the document outside of transaction + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents/target_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Try to commit transaction - should fail because document no longer exists + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response['headers']['status-code']); // Conflict + } + + /** + * Test bulk operations in transactions + */ + public function testBulkOperations(): 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' => 'BulkOpsDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); + + // Create some initial documents + for ($i = 1; $i <= 5; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'category' => 'old' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + // Bulk create + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkCreate', + 'data' => [ + ['$id' => 'bulk_1', 'name' => 'Bulk 1', 'category' => 'new'], + ['$id' => 'bulk_2', 'name' => 'Bulk 2', 'category' => 'new'], + ['$id' => 'bulk_3', 'name' => 'Bulk 3', 'category' => 'new'], + ] + ], + // Bulk update + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [Query::equal('category', ['old'])->toString()], + 'data' => ['category' => 'updated'] + ] + ], + // Bulk delete + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [Query::equal('name', ['Existing 5'])->toString()] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify results + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have 7 documents (5 existing - 1 deleted + 3 new) + $this->assertEquals(7, $documents['body']['total']); + + // Check categories were updated + $oldCategoryCount = 0; + $updatedCategoryCount = 0; + $newCategoryCount = 0; + + foreach ($documents['body']['documents'] as $doc) { + switch ($doc['category']) { + case 'old': + $oldCategoryCount++; + break; + case 'updated': + $updatedCategoryCount++; + break; + case 'new': + $newCategoryCount++; + break; + } + } + + $this->assertEquals(0, $oldCategoryCount); + $this->assertEquals(4, $updatedCategoryCount); // 4 existing docs updated + $this->assertEquals(3, $newCategoryCount); // 3 new docs + } + + /** + * Test transaction with mixed success and failure operations + */ + public function testPartialFailureRollback(): 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' => 'PartialFailureDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes with constraints + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'email', + 'size' => 256, + 'required' => true, + ]); + + // Create unique index on email + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'unique_email', + 'type' => 'unique', + 'attributes' => ['email'], + ]); + + sleep(3); + + // Create an existing document + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => ID::unique(), + 'data' => ['email' => 'existing@example.com'] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operations - mix of valid and invalid + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid1@example.com'] // Valid + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid2@example.com'] // Valid + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'existing@example.com'] // Will fail - duplicate + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid3@example.com'] // Would be valid but should rollback + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Try to commit - should fail and rollback all operations + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response['headers']['status-code']); // Conflict due to duplicate + + // Verify NO new documents were created (atomicity) + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(1, $documents['body']['total']); // Only the original document + $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + } + + /** + * Test double commit/rollback attempts + */ + public function testDoubleCommitRollback(): 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' => 'DoubleCommitDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [Permission::create(Role::any())], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Test double commit + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operation + $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['data' => 'Test'] + ] + ] + ]); + + // First commit + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Second commit attempt - should fail + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); // Bad request - already committed + + // Test double rollback + $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId2 = $transaction2['body']['$id']; + + // First rollback + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Second rollback attempt - should fail + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); // Bad request - already rolled back + } + + /** + * Test operations on non-existent documents + */ + public function testOperationsOnNonExistentDocuments(): 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' => 'NonExistentDocDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Try to update non-existent document + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'non_existent_doc', + 'data' => ['data' => 'Should fail'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); // Operation added + + // Commit should fail + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); // Document not found + + // Test delete non-existent document + $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId2 = $transaction2['body']['$id']; + + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'delete', + 'documentId' => 'non_existent_doc', + 'data' => [] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit should fail + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); // Document not found + } +} From 7eba6582c6128040cb1a5674ec4f09c486610790 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 4 Sep 2025 20:32:37 +1200 Subject: [PATCH 072/145] Fix bulk payloads --- .../Databases/Http/Transactions/Update.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index 9d4eae41c9..b8b77d79ef 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -223,10 +223,9 @@ class Update extends Action case 'bulkCreate': $dbForProject->createDocuments( $collectionId, - array_map(fn ($data) => new Document($data), $data), + $data, onNext: function (Document $document) use (&$state, $collectionId) { $state[$collectionId][$document->getId()] = $document; - } ); break; @@ -234,7 +233,7 @@ class Update extends Action case 'bulkUpdate': $dbForProject->updateDocuments( $collectionId, - $data['data'] ?? null, + new Document($data['data']), Query::parseQueries($data['queries'] ?? []) ); break; @@ -242,10 +241,9 @@ class Update extends Action case 'bulkUpsert': $dbForProject->createOrUpdateDocuments( $collectionId, - array_map(fn ($data) => new Document($data), $data), + $data, onNext: function (Document $document) use (&$state, $collectionId) { $state[$collectionId][$document->getId()] = $document; - } ); break; @@ -272,16 +270,16 @@ class Update extends Action ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); - } catch (NotFoundException) { + } catch (NotFoundException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); - throw new Exception(Exception::DOCUMENT_NOT_FOUND); - } catch (DuplicateException|ConflictException) { + throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); + } catch (DuplicateException|ConflictException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ])); - throw new Exception(Exception::TRANSACTION_CONFLICT); + throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); } catch (TransactionException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', From d28643bd2bdc0f9dfaed5985f1c7ba83b37540d5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 5 Sep 2025 01:02:30 +1200 Subject: [PATCH 073/145] Update DB --- composer.json | 2 +- composer.lock | 68 +-- .../Collections/Documents/Bulk/Upsert.php | 2 +- .../Collections/Documents/Upsert.php | 2 +- .../Databases/Http/Transactions/Update.php | 539 +++++++++++++----- .../Platform/Workers/StatsResources.php | 2 +- src/Appwrite/Platform/Workers/StatsUsage.php | 4 +- .../Transactions/TransactionsTest.php | 4 +- 8 files changed, 433 insertions(+), 190 deletions(-) diff --git a/composer.json b/composer.json index 0c662c775f..7d9176d2aa 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "1.*", + "utopia-php/database": "2.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dns": "0.3.*", diff --git a/composer.lock b/composer.lock index ed66af8572..bf76f13a3e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0da713ee5642eba1d30bc51c1a04a723", + "content-hash": "3565fcc2471b5d18a159b6da1c8fad31", "packages": [ { "name": "adhocore/jwt", @@ -3296,16 +3296,16 @@ }, { "name": "utopia-php/abuse", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be" + "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/c5e2232033b507a07f72180dc56d37e1872ee7be", - "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd591568791556d246d901d6aaf9935ab02c3f9a", + "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a", "shasum": "" }, "require": { @@ -3313,7 +3313,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "1.*" + "utopia-php/database": "2.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3341,9 +3341,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.0.0" + "source": "https://github.com/utopia-php/abuse/tree/1.0.1" }, - "time": "2025-08-13T09:12:54+00:00" + "time": "2025-09-04T12:46:54+00:00" }, { "name": "utopia-php/analytics", @@ -3393,21 +3393,21 @@ }, { "name": "utopia-php/audit", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb" + "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/c0ed75f4d068f1f6c2e7149a909490d4214e72bb", - "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", + "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "1.*" + "utopia-php/database": "2.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3434,9 +3434,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/1.0.0" + "source": "https://github.com/utopia-php/audit/tree/1.0.1" }, - "time": "2025-08-13T09:09:00+00:00" + "time": "2025-09-04T12:46:43+00:00" }, { "name": "utopia-php/cache", @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.3.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "fcd166b715a14cfea11f7a9c47d4c0076bedcecd" + "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/fcd166b715a14cfea11f7a9c47d4c0076bedcecd", - "reference": "fcd166b715a14cfea11f7a9c47d4c0076bedcecd", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e4a03ba543abc4e436ec1b450750a14bd36011d5", + "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.3.1" + "source": "https://github.com/utopia-php/database/tree/2.0.0" }, - "time": "2025-09-03T15:50:41+00:00" + "time": "2025-09-04T12:36:53+00:00" }, { "name": "utopia-php/detector", @@ -3942,16 +3942,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.23", + "version": "0.33.24", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "88e8002365c10a727014ecc56322bcd1d780ceed" + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/88e8002365c10a727014ecc56322bcd1d780ceed", - "reference": "88e8002365c10a727014ecc56322bcd1d780ceed", + "url": "https://api.github.com/repos/utopia-php/http/zipball/5112b1023342163e3fbedec99f38fc32c8700aa0", + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0", "shasum": "" }, "require": { @@ -3983,9 +3983,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.23" + "source": "https://github.com/utopia-php/http/tree/0.33.24" }, - "time": "2025-09-03T11:58:14+00:00" + "time": "2025-09-04T04:18:39+00:00" }, { "name": "utopia-php/image", @@ -4190,16 +4190,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" + "reference": "eb60a61934be1d6f2f4fdabd9903a841ba078bc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/eb60a61934be1d6f2f4fdabd9903a841ba078bc9", + "reference": "eb60a61934be1d6f2f4fdabd9903a841ba078bc9", "shasum": "" }, "require": { @@ -4207,7 +4207,7 @@ "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", - "utopia-php/database": "1.*", + "utopia-php/database": "2.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -4240,9 +4240,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.1" + "source": "https://github.com/utopia-php/migration/tree/1.0.2" }, - "time": "2025-08-28T13:41:25+00:00" + "time": "2025-09-04T12:47:05+00:00" }, { "name": "utopia-php/orchestration", 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 2debdfcf6d..618d640d95 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 @@ -161,7 +161,7 @@ class Upsert extends Action try { $modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) { - return $dbForProject->createOrUpdateDocuments( + return $dbForProject->upsertDocuments( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documents, onNext: function (Document $document) use ($plan, &$upserted) { 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 c117691348..7a6c7e960b 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 @@ -299,7 +299,7 @@ class Upsert extends Action $upserted = []; try { $dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) { - return $dbForProject->createOrUpdateDocuments( + return $dbForProject->upsertDocuments( 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), [$newDocument], onNext: function (Document $document) use (&$upserted) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index b8b77d79ef..ed277e7b2f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -12,9 +12,11 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; +use Utopia\Database\Exception\Structure; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; use Utopia\Database\Validator\UID; @@ -65,6 +67,23 @@ class Update extends Action ->callback($this->action(...)); } + /** + * @param string $transactionId + * @param bool $commit + * @param bool $rollback + * @param UtopiaResponse $response + * @param Database $dbForProject + * @param Delete $queueForDeletes + * @return void + * @throws ConflictException + * @throws Exception + * @throws \DateMalformedStringException + * @throws \Throwable + * @throws \Utopia\Database\Exception + * @throws Authorization + * @throws Structure + * @throws \Utopia\Exception + */ public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Delete $queueForDeletes): void { if (!$commit && !$rollback) { @@ -113,149 +132,38 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; - // Check if this operation depends on documents created in same transaction - $dependent = \in_array($action, ['update', 'increment', 'decrement']) - && isset($state[$collectionId][$documentId]); - - if ($dependent) { - // Don't use timestamp wrapper for dependent operations - switch ($action) { - case 'update': - // Update the state document directly - $existing = $state[$collectionId][$documentId]; - foreach ($data as $key => $value) { - $existing->setAttribute($key, $value); - } - $state[$collectionId][$documentId] = $dbForProject->updateDocument( - $collectionId, - $documentId, - $existing - ); - break; - - case 'increment': - $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( - collection: $collectionId, - id: $documentId, - attribute: $data['attribute'], - value: $data['value'] ?? 1, - max: $data['max'] ?? null - ); - break; - - case 'decrement': - $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( - collection: $collectionId, - id: $documentId, - attribute: $data['attribute'], - value: $data['value'] ?? 1, - min: $data['min'] ?? null - ); - break; - } - } else { - // Use timestamp wrapper for independent operations - $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $queueForDeletes, $action, $collectionId, $documentId, $data, &$state) { - switch ($action) { - case 'create': - if ($documentId && !isset($data['$id'])) { - $data['$id'] = $documentId; - } - $state[$collectionId][$documentId] = $dbForProject->createDocument( - $collectionId, - new Document($data), - ); - break; - - case 'update': - $document = $dbForProject->updateDocument( - $collectionId, - $documentId, - new Document($data), - ); - - if ($document->isEmpty()) { - throw new ConflictException(''); - } - - $state[$collectionId][$documentId] = $document; - break; - - case 'upsert': - $document = new Document($data); - $dbForProject->createOrUpdateDocuments( - $collectionId, - [$document], - onNext: function (Document $document) use (&$state, $collectionId) { - $state[$collectionId][$document->getId()] = $document; - } - ); - break; - - case 'delete': - $dbForProject->deleteDocument($collectionId, $documentId); - - if (isset($state[$collectionId][$documentId])) { - unset($state[$collectionId][$documentId]); - } - break; - - case 'increment': - $dbForProject->increaseDocumentAttribute( - collection: $collectionId, - id: $documentId, - attribute: $data['attribute'], - value: $data['value'] ?? 1, - max: $data['max'] ?? null - ); - break; - - case 'decrement': - $dbForProject->decreaseDocumentAttribute( - collection: $collectionId, - id: $documentId, - attribute: $data['attribute'], - value: $data['value'] ?? 1, - min: $data['min'] ?? null - ); - break; - - case 'bulkCreate': - $dbForProject->createDocuments( - $collectionId, - $data, - onNext: function (Document $document) use (&$state, $collectionId) { - $state[$collectionId][$document->getId()] = $document; - } - ); - break; - - case 'bulkUpdate': - $dbForProject->updateDocuments( - $collectionId, - new Document($data['data']), - Query::parseQueries($data['queries'] ?? []) - ); - break; - - case 'bulkUpsert': - $dbForProject->createOrUpdateDocuments( - $collectionId, - $data, - onNext: function (Document $document) use (&$state, $collectionId) { - $state[$collectionId][$document->getId()] = $document; - } - ); - break; - - case 'bulkDelete': - $dbForProject->deleteDocuments( - $collectionId, - Query::parseQueries($data['queries'] ?? []) - ); - break; - } - }); + // Execute the operation based on its type + switch ($action) { + case 'create': + $this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); + break; + case 'update': + $this->handleUpdateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); + break; + case 'upsert': + $this->handleUpsertOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); + break; + case 'delete': + $this->handleDeleteOperation($dbForProject, $collectionId, $documentId, $createdAt, $state); + break; + case 'increment': + $this->handleIncrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); + break; + case 'decrement': + $this->handleDecrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); + break; + case 'bulkCreate': + $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state); + break; + case 'bulkUpdate': + $this->handleBulkUpdateOperation($dbForProject, $collectionId, $data, $createdAt, $state); + break; + case 'bulkUpsert': + $this->handleBulkUpsertOperation($dbForProject, $collectionId, $data, $createdAt, $state); + break; + case 'bulkDelete': + $this->handleBulkDeleteOperation($dbForProject, $collectionId, $data, $createdAt, $state); + break; } } @@ -269,7 +177,6 @@ class Update extends Action $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); - } catch (NotFoundException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', @@ -290,17 +197,351 @@ class Update extends Action } if ($rollback) { - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); - $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'rolledBack', ])); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); } $response ->setStatusCode(SwooleResponse::STATUS_CODE_OK) ->dynamic($transaction, UtopiaResponse::MODEL_TRANSACTION); } -} + + /** + * Handle create operation + * @throws \Utopia\Database\Exception + */ + private function handleCreateOperation( + Database $dbForProject, + string $collectionId, + ?string $documentId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + if ($documentId && !isset($data['$id'])) { + $data['$id'] = $documentId; + } + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { + $state[$collectionId][$data['$id']] = $dbForProject->createDocument( + $collectionId, + new Document($data), + ); + }); + } + + /** + * Handle update operation + * @throws ConflictException + * @throws \Utopia\Database\Exception + */ + private function handleUpdateOperation( + Database $dbForProject, + string $collectionId, + string $documentId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $dependent = isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Update the state document directly without timestamp wrapper + $state[$collectionId][$documentId] = $dbForProject->updateDocument( + $collectionId, + $documentId, + new Document($data), + ); + return; + } + + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { + $document = $dbForProject->updateDocument( + $collectionId, + $documentId, + new Document($data), + ); + if ($document->isEmpty()) { + throw new ConflictException(''); + } + $state[$collectionId][$documentId] = $document; + }); + } + + /** + * Handle upsert operation + * @throws \Utopia\Database\Exception + */ + private function handleUpsertOperation( + Database $dbForProject, + string $collectionId, + ?string $documentId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $dependent = isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Upsert the state document directly without timestamp wrapper + $state[$collectionId][$documentId] = $dbForProject->upsertDocument( + $collectionId, + new Document($data), + ); + return; + } + + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { + $state[$collectionId][$documentId] = $dbForProject->upsertDocument( + $collectionId, + new Document($data), + ); + }); + } + + /** + * Handle delete operation + */ + private function handleDeleteOperation( + Database $dbForProject, + string $collectionId, + string $documentId, + \DateTime $createdAt, + array &$state + ): void + { + $dependent = isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Delete without timestamp wrapper + $dbForProject->deleteDocument($collectionId, $documentId); + unset($state[$collectionId][$documentId]); + return; + } + + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, &$state) { + $dbForProject->deleteDocument($collectionId, $documentId); + if (isset($state[$collectionId][$documentId])) { + unset($state[$collectionId][$documentId]); + } + }); + } + + /** + * Handle increment operation + */ + private function handleIncrementOperation( + Database $dbForProject, + string $collectionId, + string $documentId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $dependent = isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Increment without timestamp wrapper + $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + max: $data['max'] ?? null + ); + return; + } + + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data) { + $dbForProject->increaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + max: $data['max'] ?? null + ); + }); + } + + /** + * Handle decrement operation + */ + private function handleDecrementOperation( + Database $dbForProject, + string $collectionId, + string $documentId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $dependent = isset($state[$collectionId][$documentId]); + + if ($dependent) { + // Decrement without timestamp wrapper + $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + min: $data['min'] ?? null + ); + return; + } + + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data) { + $dbForProject->decreaseDocumentAttribute( + collection: $collectionId, + id: $documentId, + attribute: $data['attribute'], + value: $data['value'] ?? 1, + min: $data['min'] ?? null + ); + }); + } + + /** + * Handle bulk create operation + */ + private function handleBulkCreateOperation( + Database $dbForProject, + string $collectionId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { + $dbForProject->createDocuments( + $collectionId, + $data, + onNext: function (Document $document) use (&$state, $collectionId) { + $state[$collectionId][$document->getId()] = $document; + } + ); + }); + } + + /** + * Handle bulk update operation with manual timestamp checking + * @throws \Utopia\Database\Exception + * @throws \Utopia\Database\Exception\Query + * @throws ConflictException + */ + private function handleBulkUpdateOperation( + Database $dbForProject, + string $collectionId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $queries = Query::parseQueries($data['queries'] ?? []); + + $dbForProject->updateDocuments( + $collectionId, + new Document($data['data']), + $queries, + onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt) { + // Check if this document was created/modified in this transaction + $dependent = isset($state[$collectionId][$updated->getId()]); + + // If not in transaction state, check for timestamp conflicts + if (!$dependent) { + $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); + if ($oldUpdatedAt > $createdAt) { + throw new ConflictException('Document was updated after the request timestamp'); + } + } + + $state[$collectionId][$updated->getId()] = $updated; + } + ); + } + + /** + * Handle bulk upsert operation with manual timestamp checking + * @throws ConflictException + */ + private function handleBulkUpsertOperation( + Database $dbForProject, + string $collectionId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + // Run bulk upsert without timestamp wrapper, checking manually in callback + $dbForProject->upsertDocuments( + $collectionId, + $data, + onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) { + if ($old !== null) { + // This is an update - check if document was created/modified in this transaction + $dependent = isset($state[$collectionId][$upserted->getId()]); + + // If not in transaction state, check for timestamp conflicts + if (!$dependent) { + $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); + if ($oldUpdatedAt > $createdAt) { + throw new ConflictException('Document was updated after the request timestamp'); + } + } + } + + // If $old is null, this is a create operation - no timestamp check needed + $state[$collectionId][$upserted->getId()] = $upserted; + } + ); + } + + /** + * Handle bulk delete operation with manual timestamp checking + * @throws \Utopia\Database\Exception\Query + * @throws ConflictException + */ + private function handleBulkDeleteOperation( + Database $dbForProject, + string $collectionId, + array $data, + \DateTime $createdAt, + array &$state + ): void + { + $queries = Query::parseQueries($data['queries'] ?? []); + + $dbForProject->deleteDocuments( + $collectionId, + $queries, + onNext: function (Document $deleted, Document $old) use (&$state, $collectionId, $createdAt) { + $dependent = isset($state[$collectionId][$deleted->getId()]); + + // If not in transaction state, check for timestamp conflicts + if (!$dependent) { + $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); + if ($oldUpdatedAt > $createdAt) { + throw new ConflictException('Document was updated after the transaction operation'); + } + } + + // Remove from state after successful deletion + if (isset($state[$collectionId][$deleted->getId()])) { + unset($state[$collectionId][$deleted->getId()]); + } + } + ); + } +} \ No newline at end of file diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 98c9d01a87..6f334437b0 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -432,7 +432,7 @@ class StatsResources extends Action protected function writeDocuments(Database $dbForLogs, Document $project): void { - $dbForLogs->createOrUpdateDocuments( + $dbForLogs->upsertDocuments( 'stats', $this->documents ); diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 3610381d5a..3a615072df 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -424,7 +424,7 @@ class StatsUsage extends Action try { $dbForProject = $getProjectDB($projectStats['project']); Console::log('Processing batch with ' . count($projectStats['stats']) . ' stats'); - $dbForProject->createOrUpdateDocumentsWithIncrease('stats', 'value', $projectStats['stats']); + $dbForProject->upsertDocumentsWithIncrease('stats', 'value', $projectStats['stats']); Console::success('Batch successfully written to DB'); unset($this->projects[$sequence]); @@ -468,7 +468,7 @@ class StatsUsage extends Action try { Console::log('Processing batch with ' . count($this->statDocuments) . ' stats'); - $dbForLogs->createOrUpdateDocumentsWithIncrease( + $dbForLogs->upsertDocumentsWithIncrease( 'stats', 'value', $this->statDocuments diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php index cb0f3f3827..f1e8b64d58 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -1044,6 +1044,8 @@ class TransactionsTest extends Scope 'required' => true, ]); + sleep(2); + // Create unique index on email $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", array_merge([ 'content-type' => 'application/json', @@ -1055,7 +1057,7 @@ class TransactionsTest extends Scope 'attributes' => ['email'], ]); - sleep(3); + sleep(2); // Create an existing document $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ From c2916c7a2dd7103f15c1babd2a0544cc291667c5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 5 Sep 2025 01:04:48 +1200 Subject: [PATCH 074/145] Lint --- .../Databases/Http/Transactions/Update.php | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index ed277e7b2f..edb0e2cbaa 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -222,8 +222,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { if ($documentId && !isset($data['$id'])) { $data['$id'] = $documentId; } @@ -247,8 +246,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -286,8 +284,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -317,8 +314,7 @@ class Update extends Action string $documentId, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -347,8 +343,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -385,8 +380,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -422,8 +416,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { $dbForProject->createDocuments( $collectionId, @@ -447,8 +440,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->updateDocuments( @@ -458,7 +450,7 @@ class Update extends Action onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt) { // Check if this document was created/modified in this transaction $dependent = isset($state[$collectionId][$updated->getId()]); - + // If not in transaction state, check for timestamp conflicts if (!$dependent) { $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); @@ -466,7 +458,7 @@ class Update extends Action throw new ConflictException('Document was updated after the request timestamp'); } } - + $state[$collectionId][$updated->getId()] = $updated; } ); @@ -482,8 +474,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { // Run bulk upsert without timestamp wrapper, checking manually in callback $dbForProject->upsertDocuments( $collectionId, @@ -492,7 +483,7 @@ class Update extends Action if ($old !== null) { // This is an update - check if document was created/modified in this transaction $dependent = isset($state[$collectionId][$upserted->getId()]); - + // If not in transaction state, check for timestamp conflicts if (!$dependent) { $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); @@ -519,8 +510,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->deleteDocuments( @@ -536,7 +526,7 @@ class Update extends Action throw new ConflictException('Document was updated after the transaction operation'); } } - + // Remove from state after successful deletion if (isset($state[$collectionId][$deleted->getId()])) { unset($state[$collectionId][$deleted->getId()]); @@ -544,4 +534,4 @@ class Update extends Action } ); } -} \ No newline at end of file +} From 391942ba72d5afc38407c8db936ff4d683bdef9d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 6 Sep 2025 02:37:17 +1200 Subject: [PATCH 075/145] Fix bulk logged queries --- .../Collections/Documents/Bulk/Delete.php | 4 +- .../Collections/Documents/Bulk/Update.php | 7 +- .../Transactions/TransactionsTest.php | 1620 ++++++++++++++++- 3 files changed, 1603 insertions(+), 28 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php index b57c047a8c..82f940bb62 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php @@ -104,6 +104,8 @@ class Delete extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + $originalQueries = $queries; + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -135,7 +137,7 @@ class Delete extends Action 'transactionInternalId' => $transaction->getSequence(), 'action' => 'bulkDelete', 'data' => [ - 'queries' => $queries, + 'queries' => $originalQueries, ], ]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php index 41b8bbda74..1b9559d4b9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php @@ -116,13 +116,15 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); } + $originalQueries = $queries; + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - if ($data['$permissions']) { + if (isset($data['$permissions'])) { $validator = new Permissions(); if (!$validator->isValid($data['$permissions'])) { throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription()); @@ -157,7 +159,7 @@ class Update extends Action 'action' => 'bulkUpdate', 'data' => [ 'data' => $data, - 'queries' => $queries, + 'queries' => $originalQueries, ], ]); @@ -167,7 +169,6 @@ class Update extends Action 'transactions', $transactionId, 'operations', - 1 ); }); diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php index f1e8b64d58..9409834be0 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -19,7 +19,7 @@ class TransactionsTest extends Scope /** * Test creating a transaction */ - public function testCreate(): array + public function testCreate(): void { // Create database first $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ @@ -91,21 +91,35 @@ class TransactionsTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - - return [ - 'databaseId' => $databaseId, - 'transactionId1' => $transactionId1, - 'transactionId2' => $transactionId2 - ]; } /** - * @depends testCreate + * Test adding operations to a transaction */ - public function testAddOperations(array $data): array + public function testAddOperations(): void { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId1']; + // Create database first + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionOperationsTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; // Create a collection for testing $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ @@ -233,20 +247,109 @@ class TransactionsTest extends Scope ]); $this->assertEquals(404, $response['headers']['status-code']); - - return array_merge($data, [ - 'collectionId' => $collectionId - ]); } /** - * @depends testAddOperations + * Test committing a transaction */ - public function testCommit(array $data): void + public function testCommit(): void { - $databaseId = $data['databaseId']; - $collectionId = $data['collectionId']; - $transactionId = $data['transactionId1']; + // Create database first + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionCommitTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionCommitTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add attributes + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + 'name' => 'Test Document 2' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); // Commit the transaction $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ @@ -292,12 +395,32 @@ class TransactionsTest extends Scope } /** - * @depends testCreate + * Test rolling back a transaction */ - public function testRollback(array $data): void + public function testRollback(): void { - $databaseId = $data['databaseId']; - $transactionId = $data['transactionId2']; + // Create database first + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'TransactionRollbackTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; // Create a collection for rollback test $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ @@ -1384,4 +1507,1453 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); // Document not found } + + /** + * Test createDocument with transactionId via normal route + */ + public function testCreateDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'WriteRoutesTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $attributes = [ + ['key' => 'name', 'type' => 'string', 'size' => 256, 'required' => true], + ['key' => 'counter', 'type' => 'integer', 'required' => false, 'min' => 0, 'max' => 10000], + ['key' => 'category', 'type' => 'string', 'size' => 256, 'required' => false], + ['key' => 'data', 'type' => 'string', 'size' => 256, 'required' => false], + ]; + + foreach ($attributes as $attr) { + $type = $attr['type']; + unset($attr['type']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/{$type}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $attr); + + $this->assertEquals(202, $response['headers']['status-code']); + } + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Create document via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_from_route', + 'data' => [ + 'name' => 'Created via normal route', + 'counter' => 100, + 'category' => 'test' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Created via normal route', $response['body']['name']); + } + + /** + * Test updateDocument with transactionId via normal route + */ + public function testUpdateDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'UpdateRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_update', + 'data' => [ + 'name' => 'Original name', + 'counter' => 50, + 'category' => 'original' + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Update document via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Updated via normal route', + 'counter' => 150, + 'category' => 'updated' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should still have original values outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Original name', $response['body']['name']); + $this->assertEquals(50, $response['body']['counter']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now have updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated via normal route', $response['body']['name']); + $this->assertEquals(150, $response['body']['counter']); + } + + /** + * Test upsertDocument with transactionId via normal route + */ + public function testUpsertDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'UpsertRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Upsert document (create) via normal route with transactionId + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Created by upsert', + 'counter' => 25 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Upsert same document (update) in same transaction + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Updated by upsert', + 'counter' => 75 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); // Upsert in transaction returns 201 + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Updated by upsert', $response['body']['name']); + $this->assertEquals(75, $response['body']['counter']); + } + + /** + * Test deleteDocument with transactionId via normal route + */ + public function testDeleteDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'DeleteRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_delete', + 'data' => ['name' => 'Will be deleted'] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Delete document via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Document should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should no longer exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + /** + * Test bulkCreate with transactionId via normal route + */ + public function testBulkCreate(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkCreateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk create via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_create_1', + 'name' => 'Bulk created 1', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_2', + 'name' => 'Bulk created 2', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_3', + 'name' => 'Bulk created 3', + 'category' => 'bulk_created' + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk operations return 200 + + // Documents should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + + // Individual document check + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_create_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Verify individual documents + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_create_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("Bulk created {$i}", $response['body']['name']); + $this->assertEquals('bulk_created', $response['body']['category']); + } + } + + /** + * Test bulkUpdate with transactionId via normal route + */ + public function testBulkUpdate(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpdateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_update_' . $i, + 'data' => [ + 'name' => 'Bulk doc ' . $i, + 'category' => 'bulk_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk update via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()], + 'data' => ['category' => 'bulk_updated'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should still have original category outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now have updated category + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_updated'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + } + + /** + * Test bulkUpsert with transactionId via normal route + */ + public function testBulkUpsert(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpsertTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create one document outside transaction + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_upsert_existing', + 'data' => [ + 'name' => 'Existing doc', + 'counter' => 10 + ] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk upsert via normal route with transactionId (updates existing, creates new) + $response = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_upsert_existing', + 'name' => 'Updated existing', + 'counter' => 20 + ], + [ + '$id' => 'bulk_upsert_new', + 'name' => 'New doc', + 'counter' => 30 + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Original document should be unchanged, new document shouldn't exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Existing doc', $response['body']['name']); + $this->assertEquals(10, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Check both documents exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated existing', $response['body']['name']); + $this->assertEquals(20, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('New doc', $response['body']['name']); + $this->assertEquals(30, $response['body']['counter']); + } + + /** + * Test bulkDelete with transactionId via normal route + */ + public function testBulkDelete(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkDeleteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_delete_' . $i, + 'data' => [ + 'name' => 'Delete doc ' . $i, + 'category' => 'bulk_delete_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk delete via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk delete with transaction returns 200 + + // Documents should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now be deleted + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + } + + /** + * Test multiple single route operations in one transaction + */ + public function testMixedSingleOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'MultipleSingleRoutesDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'required' => false, + 'min' => 1, + 'max' => 10, + ]); + + sleep(3); + + // Create an existing document outside transaction for testing + $existingDoc = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_doc', + 'data' => [ + 'name' => 'Existing Document', + 'status' => 'active', + 'priority' => 5 + ] + ]); + + $this->assertEquals(201, $existingDoc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + $this->assertEquals(201, $transaction['headers']['status-code']); + + // 1. Create new document via normal route with transactionId + $response1 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_1', + 'data' => [ + 'name' => 'New Document 1', + 'status' => 'pending', + 'priority' => 1 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response1['headers']['status-code']); + + // 2. Create another document via normal route with transactionId + $response2 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_2', + 'data' => [ + 'name' => 'New Document 2', + 'status' => 'pending', + 'priority' => 2 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response2['headers']['status-code']); + + // 3. Update existing document via normal route with transactionId + $response3 = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'updated', + 'priority' => 10 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response3['headers']['status-code']); + + // 4. Update the first new document (created in same transaction) + $response4 = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'active', + 'priority' => 8 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response4['headers']['status-code']); + + // 5. Delete the second new document (created in same transaction) + $response5 = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response5['headers']['status-code']); + + // 6. Upsert a new document via normal route with transactionId + $response6 = $this->client->call(Client::METHOD_PUT, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'upserted_doc', + 'data' => [ + 'name' => 'Upserted Document', + 'status' => 'new', + 'priority' => 3 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response6['headers']['status-code']); + + // Check transaction has correct number of operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $txnDetails['headers']['status-code']); + $this->assertEquals(6, $txnDetails['body']['operations']); // 6 operations total + + // Verify nothing exists outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Existing doc should still have original values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(5, $response['body']['priority']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify final state after commit + // new_doc_1 should exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('New Document 1', $response['body']['name']); + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(8, $response['body']['priority']); + + // new_doc_2 should not exist (was deleted in transaction) + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // existing_doc should have updated values + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('updated', $response['body']['status']); + $this->assertEquals(10, $response['body']['priority']); + + // upserted_doc should exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Upserted Document', $response['body']['name']); + $this->assertEquals('new', $response['body']['status']); + $this->assertEquals(3, $response['body']['priority']); + + // Verify total document count + $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(3, $documents['body']['total']); // existing_doc, new_doc_1, upserted_doc + } + + /** + * Test mixed operations with transactions + */ + public function testMixedOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'MixedOpsTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attribute + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operation via Operations\Add endpoint + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'mixed_doc1', + 'data' => ['name' => 'Via Operations Add'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['operations']); + + // Add operation via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'mixed_doc2', + 'data' => ['name' => 'Via normal route'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Check transaction now has 2 operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(2, $txnDetails['body']['operations']); + + // Both documents shouldn't exist yet + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Both documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via Operations Add', $response['body']['name']); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via normal route', $response['body']['name']); + } } From 098b0cd02838d7baa64df61f74bff2070f3717b2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 6 Sep 2025 02:37:52 +1200 Subject: [PATCH 076/145] Fix array as doc --- .../Modules/Databases/Http/Transactions/Update.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php index edb0e2cbaa..299d0f2abf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php @@ -132,6 +132,10 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; + if ($data instanceof Document) { + $data = $data->getArrayCopy(); + } + // Execute the operation based on its type switch ($action) { case 'create': @@ -197,9 +201,11 @@ class Update extends Action } if ($rollback) { - $transaction = $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'rolledBack', - ])); + $transaction = $dbForProject->updateDocument( + 'transactions', + $transactionId, + new Document(['status' => 'rolledBack']) + ); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) @@ -441,6 +447,8 @@ class Update extends Action \DateTime $createdAt, array &$state ): void { + \var_dump($data); + $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->updateDocuments( From 812a09f5564a51a3d1f86a25bdab020a5611c120 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 6 Sep 2025 04:26:49 +1200 Subject: [PATCH 077/145] Add read-your-write support --- app/init/resources.php | 5 + src/Appwrite/Databases/TransactionManager.php | 231 ++++++++++++++++++ .../Collections/Documents/Delete.php | 26 +- .../Databases/Collections/Documents/Get.php | 15 +- .../Collections/Documents/Update.php | 14 +- .../Collections/Documents/Upsert.php | 20 +- .../Databases/Collections/Documents/XList.php | 20 +- .../Transactions/TransactionsTest.php | 16 +- 8 files changed, 320 insertions(+), 27 deletions(-) create mode 100644 src/Appwrite/Databases/TransactionManager.php diff --git a/app/init/resources.php b/app/init/resources.php index e4e8fbef5e..e3955540df 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -4,6 +4,7 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Auth; use Appwrite\Auth\Key; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\Audit; use Appwrite\Event\Build; use Appwrite\Event\Certificate; @@ -1030,3 +1031,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('transactionManager', function (Database $dbForProject) { + return new TransactionManager($dbForProject); +}, ['dbForProject']); diff --git a/src/Appwrite/Databases/TransactionManager.php b/src/Appwrite/Databases/TransactionManager.php new file mode 100644 index 0000000000..533e0e489e --- /dev/null +++ b/src/Appwrite/Databases/TransactionManager.php @@ -0,0 +1,231 @@ +dbForProject = $dbForProject; + } + + /** + * Get the current state of a transaction by replaying its operations + */ + private function getTransactionState(string $transactionId): array + { + $transaction = $this->dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') { + return []; + } + + // Fetch operations ordered by sequence to replay in exact order + $operations = $this->dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc('$createdAt'), // Ensure operations are processed in order + ]); + + $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': + if ($documentId) { + $state[$collectionId][$documentId] = [ + 'action' => 'create', + 'document' => new Document($data), + 'exists' => true + ]; + } + break; + + case 'update': + if (isset($state[$collectionId][$documentId])) { + // Update existing document in transaction state + $existingDocument = $state[$collectionId][$documentId]['document']; + foreach ($data as $key => $value) { + if ($key !== '$id') { + $existingDocument->setAttribute($key, $value); + } + } + $state[$collectionId][$documentId]['action'] = 'update'; + } else { + // Document doesn't exist in transaction state, will be merged with committed version + $state[$collectionId][$documentId] = [ + 'action' => 'update', + 'document' => new Document($data), + 'exists' => true + ]; + } + break; + + case 'upsert': + $state[$collectionId][$documentId] = [ + 'action' => 'upsert', + 'document' => new Document($data), + 'exists' => true + ]; + break; + + case 'delete': + $state[$collectionId][$documentId] = [ + 'action' => 'delete', + 'exists' => false + ]; + 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; + } + } + + return $state; + } + + /** + * Get a document with transaction-aware logic + */ + public function getDocument( + string $collectionId, + string $documentId, + ?string $transactionId = null, + array $queries = [] + ): Document { + // If no transaction, use normal database retrieval + if ($transactionId === null) { + return $this->dbForProject->getDocument($collectionId, $documentId, $queries); + } + + $state = $this->getTransactionState($transactionId); + + + // Check if document exists in transaction state + if (isset($state[$collectionId][$documentId])) { + $docState = $state[$collectionId][$documentId]; + + if (!$docState['exists']) { + // Document was deleted in transaction + return new Document(); + } + + if ($docState['action'] === 'create') { + // Document was created in transaction, return the created version + return $docState['document']; + } + + if ($docState['action'] === 'update' || $docState['action'] === 'upsert') { + // This is an update to an existing document, merge with committed version + $committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries); + if (!$committedDoc->isEmpty()) { + // Apply the updates from transaction + foreach ($docState['document']->getAttributes() as $key => $value) { + if ($key !== '$id') { + $committedDoc->setAttribute($key, $value); + } + } + return $committedDoc; + } elseif ($docState['action'] === 'upsert') { + // Upsert created a new document since committed doc doesn't exist + return $docState['document']; + } + } + } + + // Document not affected by transaction, return committed version + return $this->dbForProject->getDocument($collectionId, $documentId, $queries); + } + + /** + * List documents with transaction-aware logic + */ + 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 + $documentMap[$docId] = $docState['document']; + } 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); + } + } + } elseif ($docState['action'] === 'upsert') { + // Upsert created a new document + $documentMap[$docId] = $docState['document']; + } + } + } + } + + return array_values($documentMap); + } + + /** + * Check if a document exists with transaction-aware logic + */ + public function documentExists( + string $collectionId, + string $documentId, + ?string $transactionId = null + ): bool { + $doc = $this->getDocument($collectionId, $documentId, $transactionId); + return !$doc->isEmpty(); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 7ce1d411ce..20a7b98e61 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -79,12 +80,24 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionManager') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void - { + public function action( + string $databaseId, + string $collectionId, + string $documentId, + ?string $transactionId, + ?\DateTime $requestTimestamp, + UtopiaResponse $response, + Database $dbForProject, + Event $queueForEvents, + StatsUsage $queueForStatsUsage, + TransactionManager $transactionManager, + array $plan + ): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -101,7 +114,14 @@ class Delete extends Action } // Read permission should not be required for delete - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); + $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + + if ($transactionId !== null) { + // Use transaction-aware document retrieval to see changes from same transaction + $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + } else { + $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + } if ($document->isEmpty()) { throw new Exception($this->getNotFoundException()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index 7f621fb33a..c69181b30d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; @@ -63,13 +64,15 @@ class Get extends Action ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('documentId', '', new UID(), 'Document ID.') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('transactionManager') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager): void { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -93,13 +96,17 @@ class Get extends Action try { $selects = Query::groupByType($queries)['selections'] ?? []; + $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); - if (! empty($selects)) { + // Use transaction-aware document retrieval if transactionId is provided + if ($transactionId !== null) { + $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId, $queries); + } elseif (! empty($selects)) { // has selects, allow relationship on documents! - $document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries); + $document = $dbForProject->getDocument($collectionTableId, $documentId, $queries); } else { // has no selects, disable relationship looping on documents! - $document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries)); + $document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument($collectionTableId, $documentId, $queries)); } } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); 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 ba3ad4d8b5..16f9dab0d7 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 @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -83,13 +84,13 @@ class Update extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionManager') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager, array $plan): void { - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (empty($data) && \is_null($permissions)) { @@ -113,7 +114,14 @@ class Update extends Action // Read permission should not be required for update /** @var Document $document */ - $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); + $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + + if ($transactionId !== null) { + // Use transaction-aware document retrieval to see changes from same transaction + $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + } else { + $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + } if ($document->isEmpty()) { throw new Exception($this->getNotFoundException()); 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 7a6c7e960b..da801db618 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 @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -87,11 +88,12 @@ class Upsert extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionManager') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -124,9 +126,16 @@ class Upsert extends Action $permissions = Permission::aggregate($permissions, $allowedPermissions); + $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + // If no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario) if (\is_null($permissions)) { - $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); + if ($transactionId !== null) { + // Use transaction-aware document retrieval to see changes from same transaction + $oldDocument = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + } else { + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); + } if ($oldDocument->isEmpty()) { if (!empty($user->getId())) { $defaultPermissions = []; @@ -320,7 +329,12 @@ class Upsert extends Action $collectionsCache = []; if (empty($upserted[0])) { - $upserted[0] = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId); + if ($transactionId !== null) { + // For transactions, get the document with transaction changes applied + $upserted[0] = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + } else { + $upserted[0] = $dbForProject->getDocument($collectionTableId, $documentId); + } } $document = $upserted[0]; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 9c8405cf18..78f91033a9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionManager; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; @@ -65,13 +66,15 @@ class XList extends Action ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('transactionManager') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void + public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager): void { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -121,17 +124,22 @@ class XList extends Action try { $selectQueries = Query::groupByType($queries)['selections'] ?? []; + $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); - if (! empty($selectQueries)) { + // Use transaction-aware document retrieval if transactionId is provided + if ($transactionId !== null) { + $documents = $transactionManager->listDocuments($collectionTableId, $transactionId, $queries); + $total = count($documents); // For transaction-aware queries, we count the actual results + } elseif (! empty($selectQueries)) { // has selects, allow relationship on documents - $documents = $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries); + $documents = $dbForProject->find($collectionTableId, $queries); + $total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT); } else { // has no selects, disable relationship loading on documents /* @type Document[] $documents */ - $documents = $dbForProject->skipRelationships(fn () => $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries)); + $documents = $dbForProject->skipRelationships(fn () => $dbForProject->find($collectionTableId, $queries)); + $total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT); } - - $total = $dbForProject->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries, APP_LIMIT_COUNT); } catch (OrderException $e) { $documents = $this->isCollectionsAPI() ? 'documents' : 'rows'; $attribute = $this->isCollectionsAPI() ? 'attribute' : 'column'; diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php index 9409834be0..daba81e6ec 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -1554,7 +1554,7 @@ class TransactionsTest extends Scope foreach ($attributes as $attr) { $type = $attr['type']; unset($attr['type']); - + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/{$type}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1874,7 +1874,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Document should now exist with updated values @@ -1982,7 +1982,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Document should no longer exist @@ -2112,7 +2112,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Documents should now exist @@ -2249,7 +2249,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Documents should now have updated category @@ -2389,7 +2389,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Check both documents exist with updated values @@ -2520,7 +2520,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Documents should now be deleted @@ -2936,7 +2936,7 @@ class TransactionsTest extends Scope ]), [ 'commit' => true ]); - + $this->assertEquals(200, $response['headers']['status-code']); // Both documents should now exist From abf6c0a0b8f3be38b5ba924862907ddebc94b0cc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Sep 2025 19:20:26 +1200 Subject: [PATCH 078/145] Add more bulk tests --- .../Transactions/TransactionsTest.php | 679 ++++++++++++++++++ 1 file changed, 679 insertions(+) diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php index daba81e6ec..e2d39d75ec 100644 --- a/tests/e2e/Services/Databases/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Transactions/TransactionsTest.php @@ -2956,4 +2956,683 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Via normal route', $response['body']['name']); } + + /** + * Test bulk update with queries that should match documents created in the same transaction + */ + public function testBulkUpdateWithTransactionAwareQueries(): 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' => 'BulkTxnAwareDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'age', + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for attributes to be created + + // Create some existing documents + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'age' => 20 + $i, + 'status' => 'inactive' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Create new documents with age > 25 in transaction + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'txn_doc_1', + 'data' => [ + 'name' => 'Transaction Doc 1', + 'age' => 30, + 'status' => 'inactive' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'txn_doc_2', + 'data' => [ + 'name' => 'Transaction Doc 2', + 'age' => 35, + 'status' => 'inactive' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Step 2: Bulk update all documents with age > 25 to have status 'active' + // This should match both existing_3 (age=23 doesn't match, age=24 doesn't match, but existing documents have age 21,22,23) + // Wait, let me fix the ages - existing docs have ages 21, 22, 23, so only txn docs should match + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'active' + ], + 'queries' => [Query::greaterThan('age', 25)->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify that documents created in the transaction were updated by the bulk update + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/txn_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/txn_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); + + // Verify existing documents were not affected + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('inactive', $response['body']['status'], "Existing document {$i} should remain inactive (age <= 25)"); + } + } + + /** + * Test bulk update with queries that should match documents updated in the same transaction + */ + public function testBulkUpdateMatchingUpdatedDocuments(): 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' => 'BulkUpdateTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for attributes to be created + + // Create existing documents + for ($i = 1; $i <= 4; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_' . $i, + 'data' => [ + 'name' => 'Document ' . $i, + 'category' => 'normal', + 'priority' => 'low' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Update some documents to have category 'special' in transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'category' => 'special' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'category' => 'special' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Step 2: Bulk update all documents with category 'special' to have priority 'high' + // This should match the documents we just updated in the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'priority' => 'high' + ], + 'queries' => [Query::equal('category', ['special'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify that the updated documents were matched by bulk update + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('special', $response['body']['category']); + $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('special', $response['body']['category']); + $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); + + // Verify other documents were not affected + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_3", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('normal', $response['body']['category']); + $this->assertEquals('low', $response['body']['priority']); + } + + /** + * Test bulk delete with queries that should match documents created in the same transaction + */ + public function testBulkDeleteMatchingCreatedDocuments(): 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' => 'BulkDeleteTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'type', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for attributes to be created + + // Create existing documents + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'type' => 'permanent' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Create temporary documents in transaction + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'temp_1', + 'data' => [ + 'name' => 'Temporary 1', + 'type' => 'temporary' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'temp_2', + 'data' => [ + 'name' => 'Temporary 2', + 'type' => 'temporary' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Step 2: Bulk delete all documents with type 'temporary' + // This should delete the documents we just created in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('type', ['temporary'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify temporary documents were deleted (should not exist) + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/temp_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/temp_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + + // Verify existing documents were not affected + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/existing_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code'], "Permanent document {$i} should still exist"); + $this->assertEquals('permanent', $response['body']['type']); + } + } + + /** + * Test bulk delete with queries that should match documents updated in the same transaction + */ + public function testBulkDeleteMatchingUpdatedDocuments(): 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' => 'BulkDeleteUpdateTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for attributes to be created + + // Create existing documents + for ($i = 1; $i <= 5; $i++) { + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_' . $i, + 'data' => [ + 'name' => 'Document ' . $i, + 'status' => 'active' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Mark some documents for deletion by updating their status + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'marked_for_deletion' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_4", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'marked_for_deletion' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Step 2: Bulk delete all documents with status 'marked_for_deletion' + // This should delete the documents we just updated in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('status', ['marked_for_deletion'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify marked documents were deleted + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); + + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_4", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); + + // Verify other documents still exist + foreach ([1, 3, 5] as $i) { + $response = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/doc_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code'], "Document {$i} should still exist"); + $this->assertEquals('active', $response['body']['status']); + } + } } From 7c3ba7471062aac5dd8a76a325654669ed86f232 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 8 Sep 2025 19:21:59 +1200 Subject: [PATCH 079/145] Rename --- app/init/resources.php | 6 +++--- src/Appwrite/Databases/TransactionManager.php | 2 +- .../Http/Databases/Collections/Documents/Delete.php | 8 ++++---- .../Http/Databases/Collections/Documents/Get.php | 8 ++++---- .../Http/Databases/Collections/Documents/Update.php | 8 ++++---- .../Http/Databases/Collections/Documents/Upsert.php | 10 +++++----- .../Http/Databases/Collections/Documents/XList.php | 8 ++++---- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index e3955540df..2f158a7cdf 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -4,7 +4,7 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Auth; use Appwrite\Auth\Key; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\Audit; use Appwrite\Event\Build; use Appwrite\Event\Certificate; @@ -1032,6 +1032,6 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef return $referrer; }, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']); -App::setResource('transactionManager', function (Database $dbForProject) { - return new TransactionManager($dbForProject); +App::setResource('transactionState', function (Database $dbForProject) { + return new TransactionState($dbForProject); }, ['dbForProject']); diff --git a/src/Appwrite/Databases/TransactionManager.php b/src/Appwrite/Databases/TransactionManager.php index 533e0e489e..7d99d61d01 100644 --- a/src/Appwrite/Databases/TransactionManager.php +++ b/src/Appwrite/Databases/TransactionManager.php @@ -9,7 +9,7 @@ use Utopia\Database\Query; /** * Service for managing transaction state and providing transaction-aware document operations */ -class TransactionManager +class TransactionState { private Database $dbForProject; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 20a7b98e61..8459e06c43 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -80,7 +80,7 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('transactionManager') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } @@ -95,7 +95,7 @@ class Delete extends Action Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, - TransactionManager $transactionManager, + TransactionState $transactionState, array $plan ): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -118,7 +118,7 @@ class Delete extends Action if ($transactionId !== null) { // Use transaction-aware document retrieval to see changes from same transaction - $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index c69181b30d..cced1440a5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; @@ -68,11 +68,11 @@ class Get extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') - ->inject('transactionManager') + ->inject('transactionState') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager): void + public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -100,7 +100,7 @@ class Get extends Action // Use transaction-aware document retrieval if transactionId is provided if ($transactionId !== null) { - $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId, $queries); + $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId, $queries); } elseif (! empty($selects)) { // has selects, allow relationship on documents! $document = $dbForProject->getDocument($collectionTableId, $documentId, $queries); 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 16f9dab0d7..a2376b44a2 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 @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -84,12 +84,12 @@ class Update extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('transactionManager') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -118,7 +118,7 @@ class Update extends Action if ($transactionId !== null) { // Use transaction-aware document retrieval to see changes from same transaction - $document = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { $document = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } 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 da801db618..6690e0214f 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 @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -88,12 +88,12 @@ class Upsert extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') - ->inject('transactionManager') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager, array $plan): void + public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan): void { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -132,7 +132,7 @@ class Upsert extends Action if (\is_null($permissions)) { if ($transactionId !== null) { // Use transaction-aware document retrieval to see changes from same transaction - $oldDocument = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + $oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId)); } @@ -331,7 +331,7 @@ class Upsert extends Action if (empty($upserted[0])) { if ($transactionId !== null) { // For transactions, get the document with transaction changes applied - $upserted[0] = $transactionManager->getDocument($collectionTableId, $documentId, $transactionId); + $upserted[0] = $transactionState->getDocument($collectionTableId, $documentId, $transactionId); } else { $upserted[0] = $dbForProject->getDocument($collectionTableId, $documentId); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 78f91033a9..7e6931395e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; use Appwrite\Auth\Auth; -use Appwrite\Databases\TransactionManager; +use Appwrite\Databases\TransactionState; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; @@ -70,11 +70,11 @@ class XList extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') - ->inject('transactionManager') + ->inject('transactionState') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionManager $transactionManager): void + public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -128,7 +128,7 @@ class XList extends Action // Use transaction-aware document retrieval if transactionId is provided if ($transactionId !== null) { - $documents = $transactionManager->listDocuments($collectionTableId, $transactionId, $queries); + $documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries); $total = count($documents); // For transaction-aware queries, we count the actual results } elseif (! empty($selectQueries)) { // has selects, allow relationship on documents From 495eeeb9296a3e00ea2714c42aab92deba16747c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 9 Sep 2025 00:50:48 +1200 Subject: [PATCH 080/145] Support legacy --- .github/workflows/tests.yml | 1 - ...actionManager.php => TransactionState.php} | 0 .../{ => Databases}/Transactions/Create.php | 2 +- .../{ => Databases}/Transactions/Delete.php | 2 +- .../Http/{ => Databases}/Transactions/Get.php | 2 +- .../Transactions/Operations/Create.php | 4 +- .../{ => Databases}/Transactions/Update.php | 4 +- .../{ => Databases}/Transactions/XList.php | 2 +- .../Http/TablesDB/Transactions/Create.php | 54 + .../Http/TablesDB/Transactions/Delete.php | 54 + .../Http/TablesDB/Transactions/Get.php | 54 + .../Transactions/Operations/Create.php | 58 + .../Http/TablesDB/Transactions/Update.php | 58 + .../Http/TablesDB/Transactions/XList.php | 54 + .../Modules/Databases/Services/Http.php | 1 - .../Databases/Services/Registry/Legacy.php | 17 + .../Databases/Services/Registry/TablesDB.php | 17 + .../Services/Registry/Transactions.php | 27 - .../Utopia/Database/Validator/Operation.php | 26 +- .../DatabasesPermissionsGuestTest.php | 2 +- .../DatabasesPermissionsMemberTest.php | 2 +- .../DatabasesPermissionsScope.php | 2 +- .../DatabasesPermissionsTeamTest.php | 2 +- .../{ => Legacy}/Transactions/ACIDTest.php | 3 +- .../Transactions/TransactionsTest.php | 2 +- .../DatabasesPermissionsGuestTest.php | 2 +- .../DatabasesPermissionsMemberTest.php | 2 +- .../DatabasesPermissionsScope.php | 2 +- .../DatabasesPermissionsTeamTest.php | 2 +- .../TablesDB/Transactions/ACIDTest.php | 625 +++ .../Transactions/TransactionsTest.php | 3638 +++++++++++++++++ 31 files changed, 4670 insertions(+), 51 deletions(-) rename src/Appwrite/Databases/{TransactionManager.php => TransactionState.php} (100%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/Create.php (97%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/Delete.php (97%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/Get.php (96%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/Operations/Create.php (97%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/Update.php (99%) rename src/Appwrite/Platform/Modules/Databases/Http/{ => Databases}/Transactions/XList.php (97%) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php delete mode 100644 src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php rename tests/e2e/Services/Databases/Legacy/{ => Permissions}/DatabasesPermissionsGuestTest.php (99%) rename tests/e2e/Services/Databases/Legacy/{ => Permissions}/DatabasesPermissionsMemberTest.php (99%) rename tests/e2e/Services/Databases/{TablesDB => Legacy/Permissions}/DatabasesPermissionsScope.php (97%) rename tests/e2e/Services/Databases/Legacy/{ => Permissions}/DatabasesPermissionsTeamTest.php (99%) rename tests/e2e/Services/Databases/{ => Legacy}/Transactions/ACIDTest.php (99%) rename tests/e2e/Services/Databases/{ => Legacy}/Transactions/TransactionsTest.php (99%) rename tests/e2e/Services/Databases/TablesDB/{ => Permissions}/DatabasesPermissionsGuestTest.php (99%) rename tests/e2e/Services/Databases/TablesDB/{ => Permissions}/DatabasesPermissionsMemberTest.php (99%) rename tests/e2e/Services/Databases/{Legacy => TablesDB/Permissions}/DatabasesPermissionsScope.php (97%) rename tests/e2e/Services/Databases/TablesDB/{ => Permissions}/DatabasesPermissionsTeamTest.php (99%) create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2ca0c58db8..7b1a243da1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -169,7 +169,6 @@ jobs: Console, Databases/Legacy, Databases/TablesDB, - Databases/Transactions, Functions, FunctionsSchedule, GraphQL, diff --git a/src/Appwrite/Databases/TransactionManager.php b/src/Appwrite/Databases/TransactionState.php similarity index 100% rename from src/Appwrite/Databases/TransactionManager.php rename to src/Appwrite/Databases/TransactionState.php diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php similarity index 97% rename from src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php rename to src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index 2c98565568..d2f114a888 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -1,6 +1,6 @@ param('transactionId', '', new UID(), 'Transaction ID.') - ->param('operations', [], new ArrayList(new Operation()), 'Array of staged operations.', true) + ->param('operations', [], new ArrayList(new Operation(type: 'legacy')), 'Array of staged operations.', true) ->inject('response') ->inject('dbForProject') ->inject('plan') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php similarity index 99% rename from src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php rename to src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 299d0f2abf..e854306a47 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -1,6 +1,6 @@ setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/transactions') + ->desc('Create transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'createTransaction', + description: '/docs/references/tablesdb/create-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_CREATED, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true) + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php new file mode 100644 index 0000000000..9440b586c5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php @@ -0,0 +1,54 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/tablesdb/transactions/:transactionId') + ->desc('Delete transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'deleteTransaction', + description: '/docs/references/tablesdb/delete-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_NOCONTENT, + model: UtopiaResponse::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php new file mode 100644 index 0000000000..a5e9c374a6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php @@ -0,0 +1,54 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/tablesdb/transactions/:transactionId') + ->desc('Get transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'getTransaction', + description: '/docs/references/tablesdb/get-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php new file mode 100644 index 0000000000..eac524682c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -0,0 +1,58 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/tablesdb/transactions/:transactionId/operations') + ->desc('Add operations to transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'createOperations', + description: '/docs/references/tablesdb/create-operations.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_CREATED, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('operations', [], new ArrayList(new Operation(type: 'tablesdb')), 'Array of staged operations.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('plan') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php new file mode 100644 index 0000000000..d5b0e737a0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -0,0 +1,58 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/tablesdb/transactions/:transactionId') + ->desc('Update transaction') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'updateTransaction', + description: '/docs/references/tablesdb/update-transaction.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION, + ) + ], + contentType: ContentType::JSON + )) + ->param('transactionId', '', new UID(), 'Transaction ID.') + ->param('commit', false, new Boolean(), 'Commit transaction?', true) + ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php new file mode 100644 index 0000000000..e04d28f200 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php @@ -0,0 +1,54 @@ +setHttpMethod(self::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/tablesdb/transactions') + ->desc('List transactions') + ->groups(['api', 'database', 'transactions']) + ->label('scope', 'transactions.read') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('sdk', new Method( + namespace: 'tablesDB', + group: 'transactions', + name: 'listTransactions', + description: '/docs/references/tablesdb/list-transactions.md', + auth: [AuthType::KEY], + responses: [ + new SDKResponse( + code: SwooleResponse::STATUS_CODE_OK, + model: UtopiaResponse::MODEL_TRANSACTION_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->param('queries', [], new Transactions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries).', true) + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Http.php b/src/Appwrite/Platform/Modules/Databases/Services/Http.php index e5a2d92b40..86f48c42bb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php @@ -19,7 +19,6 @@ class Http extends Service foreach ([ LegacyRegistry::class, TablesDBRegistry::class, - TransactionsRegistry::class, ] as $registrar) { new $registrar($this); } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php index 9db5e00f8f..3286aecf93 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php @@ -58,6 +58,12 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Create as CreateDatabase; use Appwrite\Platform\Modules\Databases\Http\Databases\Delete as DeleteDatabase; use Appwrite\Platform\Modules\Databases\Http\Databases\Get as GetDatabase; use Appwrite\Platform\Modules\Databases\Http\Databases\Logs\XList as ListDatabaseLogs; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Create as CreateTransaction; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Delete as DeleteTransaction; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Get as GetTransaction; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations\Create as CreateOperations; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Update as UpdateTransaction; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\XList as ListTransactions; use Appwrite\Platform\Modules\Databases\Http\Databases\Update as UpdateDatabase; use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\Get as GetDatabaseUsage; use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\XList as ListDatabaseUsage; @@ -82,6 +88,7 @@ class Legacy extends Base $this->registerDocumentActions($service); $this->registerAttributeActions($service); $this->registerIndexActions($service); + $this->registerTransactionActions($service); } public function registerDatabaseActions(Service $service): void @@ -191,4 +198,14 @@ class Legacy extends Base $service->addAction(DeleteIndex::getName(), new DeleteIndex()); $service->addAction(ListIndexes::getName(), new ListIndexes()); } + + private function registerTransactionActions(Service $service): void + { + $service->addAction(CreateTransaction::getName(), new CreateTransaction()); + $service->addAction(GetTransaction::getName(), new GetTransaction()); + $service->addAction(UpdateTransaction::getName(), new UpdateTransaction()); + $service->addAction(DeleteTransaction::getName(), new DeleteTransaction()); + $service->addAction(ListTransactions::getName(), new ListTransactions()); + $service->addAction(CreateOperations::getName(), new CreateOperations()); + } } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php index 11be613629..4a02ac684e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php @@ -57,6 +57,12 @@ use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Rows\XList as ListR use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Update as UpdateTable; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Usage\Get as GetTableUsage; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\XList as ListTables; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Create as CreateTransaction; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Delete as DeleteTransaction; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Get as GetTransaction; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Operations\Create as CreateOperations; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Update as UpdateTransaction; +use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\XList as ListTransactions; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Update as UpdateTablesDatabase; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Usage\Get as GetTablesDatabaseUsage; use Appwrite\Platform\Modules\Databases\Http\TablesDB\Usage\XList as ListTablesDatabaseUsage; @@ -81,6 +87,7 @@ class TablesDB extends Base $this->registerColumnActions($service); $this->registerIndexActions($service); $this->registerRowActions($service); + $this->registerTransactionActions($service); } private function registerDatabaseActions(Service $service): void @@ -188,4 +195,14 @@ class TablesDB extends Base $service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn()); $service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn()); } + + private function registerTransactionActions(Service $service): void + { + $service->addAction(CreateTransaction::getName(), new CreateTransaction()); + $service->addAction(GetTransaction::getName(), new GetTransaction()); + $service->addAction(UpdateTransaction::getName(), new UpdateTransaction()); + $service->addAction(DeleteTransaction::getName(), new DeleteTransaction()); + $service->addAction(ListTransactions::getName(), new ListTransactions()); + $service->addAction(CreateOperations::getName(), new CreateOperations()); + } } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php deleted file mode 100644 index b41d6adcdb..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Transactions.php +++ /dev/null @@ -1,27 +0,0 @@ -addAction(CreateTransaction::getName(), new CreateTransaction()); - $service->addAction(GetTransaction::getName(), new GetTransaction()); - $service->addAction(UpdateTransaction::getName(), new UpdateTransaction()); - $service->addAction(DeleteTransaction::getName(), new DeleteTransaction()); - $service->addAction(ListTransactions::getName(), new ListTransactions()); - $service->addAction(CreateOperations::getName(), new CreateOperations()); - } -} diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 594648db3e..ec2d4f0d49 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -11,7 +11,6 @@ class Operation extends Validator /** @var array */ private array $required = [ 'databaseId', - 'collectionId', 'action', ]; @@ -39,6 +38,27 @@ class Operation extends Validator 'bulkDelete' => true, ]; + private string $collectionIdName = ''; + private string $documentIdName = ''; + + public function __construct(private readonly string $type) + { + switch ($this->type) { + case 'legacy': + $this->collectionIdName = 'collectionId'; + $this->documentIdName = 'documentId'; + break; + case 'tablesdb': + $this->collectionIdName = 'tableId'; + $this->documentIdName = 'rowId'; + break; + default: + throw new \InvalidArgumentException('Invalid type provided.'); + } + + $this->required[] = $this->collectionIdName; + } + public function getDescription(): string { return $this->description; @@ -85,9 +105,9 @@ class Operation extends Validator // If action requires documentId, it must be present if ( isset($this->requiresDocumentId[$value['action']]) && - !\array_key_exists('documentId', $value) + !\array_key_exists($this->documentIdName, $value) ) { - $this->description = "Key 'documentId' is required for action '{$value['action']}'"; + $this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'"; return false; } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php similarity index 99% rename from tests/e2e/Services/Databases/Legacy/DatabasesPermissionsGuestTest.php rename to tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php index abeef6c222..6496aa285a 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/Legacy/Permissions/DatabasesPermissionsGuestTest.php @@ -1,6 +1,6 @@ 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' => 'AtomicityTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with unique constraint + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'AtomicityTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add unique column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'email', + 'size' => 256, + 'required' => true, + ]); + + // Add unique index + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'unique_email', + 'type' => Database::INDEX_UNIQUE, + 'columns' => ['email'] + ]); + + sleep(3); + + // Create first document outside transaction + $doc1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'existing@example.com' + ] + ]); + + $this->assertEquals(201, $doc1['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction)); + $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body'])); + $transactionId = $transaction['body']['$id']; + + // Add operations - second one will fail due to unique constraint + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'newuser@example.com' // This should succeed + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'existing@example.com' // This will fail - duplicate + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'email' => 'anotheruser@example.com' // This should not be created due to atomicity + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code'], 'Add operations failed. Response: ' . json_encode($response['body'])); + + // Attempt to commit - should fail due to unique constraint violation + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + if ($response['headers']['status-code'] === 200) { + // If transaction succeeded, all documents should be created + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have 4 documents total (1 original + 3 from transaction) + // But since we have a unique constraint violation, this might fail + $this->assertGreaterThanOrEqual(1, $documents['body']['total']); + } else { + $this->assertEquals(409, $response['headers']['status-code']); // Conflict error + + // Verify NO new documents were created (atomicity) + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(1, $documents['body']['total']); // Only the original document + $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + } + } + + /** + * Test consistency - schema validation and constraints + */ + public function testConsistency(): void + { + // Create database + $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' => 'ConsistencyTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with required fields and constraints + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'ConsistencyTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add required string column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'required_field', + 'size' => 256, + 'required' => true, + ]); + + // Add integer column with min/max constraints + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'age', + 'required' => true, + 'min' => 18, + 'max' => 100 + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operations with both valid and invalid data + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Valid User', + 'age' => 25 // Valid age + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Too Young User', + 'age' => 10 // Below minimum - will fail constraint + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => [ + 'required_field' => 'Another Valid User', + 'age' => 30 // Valid but should not be created due to transaction failure + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Attempt to commit - should fail due to constraint violation + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertContains($response['headers']['status-code'], [400, 500], 'Transaction commit should fail due to validation. Response: ' . json_encode($response['body'])); + + // Verify no documents were created + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(0, $documents['body']['total']); + } + + /** + * Test isolation - concurrent transactions on same data + */ + public function testIsolation(): void + { + // Create database + $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' => 'IsolationTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'IsolationTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add counter column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => true, + 'min' => 0, + 'max' => 1000000 + ]); + + sleep(2); + + // Create initial document with counter + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'shared_counter', + 'data' => [ + 'counter' => 0 + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create first transaction + $transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed'); + $this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id'); + $transactionId1 = $transaction1['body']['$id']; + + // Create second transaction + $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed'); + $this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id'); + $transactionId2 = $transaction2['body']['$id']; + + // Transaction 1: Increment counter by 10 + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId1}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'shared_counter', + 'action' => 'increment', + 'data' => [ + 'column' => 'counter', + 'value' => 10 + ] + ] + ] + ]); + + // Transaction 2: Increment counter by 5 + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'shared_counter', + 'action' => 'increment', + 'data' => [ + 'column' => 'counter', + 'value' => 5 + ] + ] + ] + ]); + + // Commit first transaction + $response1 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId1}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response1['headers']['status-code']); + + // Commit second transaction + $response2 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response2['headers']['status-code']); + + // Check final value - both increments should be applied + $document = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/shared_counter", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Both increments should be applied: 0 + 10 + 5 = 15 + $this->assertEquals(15, $document['body']['counter']); + } + + /** + * Test durability - committed data persists + */ + public function testDurability(): void + { + // Create database + $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' => 'DurabilityTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'DurabilityTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create and commit transaction with multiple operations + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed'); + $this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id'); + $transactionId = $transaction['body']['$id']; + + // Add multiple operations + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'durable_doc_1', + 'data' => [ + 'data' => 'Important data 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'durable_doc_2', + 'data' => [ + 'data' => 'Important data 2' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'durable_doc_1', + 'data' => [ + 'data' => 'Updated important data 1' + ] + ] + ] + ]); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body'])); + $this->assertEquals('committed', $response['body']['status']); + + // List all documents to see what was created + $allDocs = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body'])); + + // Verify documents exist and have correct data + $document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $document1['headers']['status-code']); + $this->assertEquals('Updated important data 1', $document1['body']['data']); + + $document2 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $document2['headers']['status-code']); + $this->assertEquals('Important data 2', $document2['body']['data']); + + // Further update outside transaction to ensure persistence + $update = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'data' => 'Modified outside transaction' + ] + ]); + + $this->assertEquals(200, $update['headers']['status-code']); + + // Verify the update persisted + $document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Modified outside transaction', $document1['body']['data']); + + // List all documents to verify total count + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(2, $documents['body']['total']); + } +} diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php new file mode 100644 index 0000000000..7d19a65648 --- /dev/null +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -0,0 +1,3638 @@ +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' => 'TransactionTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Test creating a transaction with default TTL + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('status', $response['body']); + $this->assertArrayHasKey('operations', $response['body']); + $this->assertArrayHasKey('expiresAt', $response['body']); + $this->assertEquals('pending', $response['body']['status']); + $this->assertEquals(0, $response['body']['operations']); + + $transactionId1 = $response['body']['$id']; + + // Test creating a transaction with custom TTL + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 900 + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals('pending', $response['body']['status']); + + $expiresAt = new \DateTime($response['body']['expiresAt']); + $now = new \DateTime(); + $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThan(800, $diff); + $this->assertLessThan(1000, $diff); + + $transactionId2 = $response['body']['$id']; + + // Test invalid TTL values + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 30 // Below minimum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 4000 // Above maximum + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test adding operations to a transaction + */ + public function testAddOperations(): void + { + // Create database first + $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' => 'TransactionOperationsTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Create a collection for testing + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionOperationsTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add columns + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $column['headers']['status-code']); + + // Wait for column to be created + sleep(2); + + // Add valid operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + 'name' => 'Test Document 2' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['operations']); + + // Test adding more operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); + + // Test invalid database ID + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => 'invalid_database', + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); + + // Test invalid collection ID + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => 'invalid_collection', + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + /** + * Test committing a transaction + */ + public function testCommit(): void + { + // Create database first + $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' => 'TransactionCommitTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionCommitTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add columns + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $column['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Test Document 1' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc2', + 'data' => [ + 'name' => 'Test Document 2' + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'doc1', + 'data' => [ + 'name' => 'Updated Document 1' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(3, $response['body']['operations']); + + // Commit the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify documents were created + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(2, $documents['body']['total']); + + // Verify the update was applied + $doc1Found = false; + foreach ($documents['body']['documents'] as $doc) { + if ($doc['$id'] === 'doc1') { + $this->assertEquals('Updated Document 1', $doc['name']); + $doc1Found = true; + } + } + $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); + + // Test committing already committed transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test rolling back a transaction + */ + public function testRollback(): void + { + // Create database first + $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' => 'TransactionRollbackTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Create a collection for rollback test + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TransactionRollbackTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add column + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Add operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'rollback_doc', + 'data' => [ + 'value' => 'Should not exist' + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Rollback the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('rolledBack', $response['body']['status']); + + // Verify no documents were created + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals(0, $documents['body']['total']); + } + + /** + * Test transaction expiration + */ + public function testTransactionExpiration(): void + { + // Create database and collection + $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' => 'ExpirationTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction with minimum TTL (60 seconds) + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'ttl' => 60 + ]); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Add operation + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['data' => 'Should expire'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Verify transaction was created with correct expiration + $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $txnDetails['headers']['status-code']); + $this->assertEquals('pending', $txnDetails['body']['status']); + + // Verify expiration time is approximately 60 seconds from now + $expiresAt = new \DateTime($txnDetails['body']['expiresAt']); + $now = new \DateTime(); + $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); + $this->assertGreaterThan(55, $diff); + $this->assertLessThan(65, $diff); + } + + /** + * Test maximum operations per transaction + */ + public function testTransactionSizeLimit(): void + { + // Create database and collection + $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' => 'SizeLimitTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [Permission::create(Role::any())], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Try to add operations exceeding the limit (assuming limit is 100) + // We'll add 50 operations twice to test incremental limit + $operations = []; + for ($i = 0; $i < 50; $i++) { + $operations[] = [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc_' . $i, + 'data' => ['value' => 'Test ' . $i] + ]; + } + + // First batch should succeed + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => $operations + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(50, $response['body']['operations']); + + // Second batch of 50 more operations + $operations = []; + for ($i = 50; $i < 100; $i++) { + $operations[] = [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => 'doc_' . $i, + 'action' => 'create', + 'data' => ['value' => 'Test ' . $i] + ]; + } + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => $operations + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(100, $response['body']['operations']); + + // Try to add one more operation - should fail + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'doc_overflow', + 'data' => ['value' => 'This should fail'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test concurrent transactions with conflicting operations + */ + public function testConcurrentTransactionConflicts(): void + { + // Create database and collection + $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' => 'ConflictTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => true, + 'min' => 0, + 'max' => 1000000, + ]); + + sleep(2); + + // Create initial document + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'shared_doc', + 'data' => ['counter' => 100] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create two transactions + $txn1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $txn2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId1 = $txn1['body']['$id']; + $transactionId2 = $txn2['body']['$id']; + + // Both transactions try to update the same document + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId1}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'shared_doc', + 'data' => ['counter' => 200] + ] + ] + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'shared_doc', + 'data' => ['counter' => 300] + ] + ] + ]); + + // Commit first transaction + $response1 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId1}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response1['headers']['status-code']); + + // Commit second transaction - should fail with conflict + $response2 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response2['headers']['status-code']); // Conflict + + // Verify the document has the value from first transaction + $doc = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/shared_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['body']['counter']); + } + + /** + * Test deleting a document that's being updated in a transaction + */ + public function testDeleteDocumentDuringTransaction(): void + { + // Create database and collection + $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' => 'DeleteConflictDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create document + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'target_doc', + 'data' => ['data' => 'Original'] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add update operation to transaction + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'target_doc', + 'data' => ['data' => 'Updated in transaction'] + ] + ] + ]); + + // Delete the document outside of transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/target_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Try to commit transaction - should fail because document no longer exists + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response['headers']['status-code']); // Conflict + } + + /** + * Test bulk operations in transactions + */ + public function testBulkOperations(): void + { + // Create database and collection + $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' => 'BulkOpsDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); + + // Create some initial documents + for ($i = 1; $i <= 5; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'category' => 'old' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + // Bulk create + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkCreate', + 'data' => [ + ['$id' => 'bulk_1', 'name' => 'Bulk 1', 'category' => 'new'], + ['$id' => 'bulk_2', 'name' => 'Bulk 2', 'category' => 'new'], + ['$id' => 'bulk_3', 'name' => 'Bulk 3', 'category' => 'new'], + ] + ], + // Bulk update + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [Query::equal('category', ['old'])->toString()], + 'data' => ['category' => 'updated'] + ] + ], + // Bulk delete + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [Query::equal('name', ['Existing 5'])->toString()] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify results + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have 7 documents (5 existing - 1 deleted + 3 new) + $this->assertEquals(7, $documents['body']['total']); + + // Check categories were updated + $oldCategoryCount = 0; + $updatedCategoryCount = 0; + $newCategoryCount = 0; + + foreach ($documents['body']['documents'] as $doc) { + switch ($doc['category']) { + case 'old': + $oldCategoryCount++; + break; + case 'updated': + $updatedCategoryCount++; + break; + case 'new': + $newCategoryCount++; + break; + } + } + + $this->assertEquals(0, $oldCategoryCount); + $this->assertEquals(4, $updatedCategoryCount); // 4 existing docs updated + $this->assertEquals(3, $newCategoryCount); // 3 new docs + } + + /** + * Test transaction with mixed success and failure operations + */ + public function testPartialFailureRollback(): void + { + // Create database and collection + $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' => 'PartialFailureDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns with constraints + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'email', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create unique index on email + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/indexes", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'unique_email', + 'type' => 'unique', + 'columns' => ['email'], + ]); + + sleep(2); + + // Create an existing document + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => ID::unique(), + 'data' => ['email' => 'existing@example.com'] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operations - mix of valid and invalid + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid1@example.com'] // Valid + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid2@example.com'] // Valid + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'existing@example.com'] // Will fail - duplicate + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['email' => 'valid3@example.com'] // Would be valid but should rollback + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Try to commit - should fail and rollback all operations + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(409, $response['headers']['status-code']); // Conflict due to duplicate + + // Verify NO new documents were created (atomicity) + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(1, $documents['body']['total']); // Only the original document + $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + } + + /** + * Test double commit/rollback attempts + */ + public function testDoubleCommitRollback(): void + { + // Create database and collection + $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' => 'DoubleCommitDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [Permission::create(Role::any())], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Test double commit + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operation + $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => ID::unique(), + 'data' => ['data' => 'Test'] + ] + ] + ]); + + // First commit + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Second commit attempt - should fail + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); // Bad request - already committed + + // Test double rollback + $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId2 = $transaction2['body']['$id']; + + // First rollback + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Second rollback attempt - should fail + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rollback' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); // Bad request - already rolled back + } + + /** + * Test operations on non-existent documents + */ + public function testOperationsOnNonExistentDocuments(): void + { + // Create database and collection + $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' => 'NonExistentDocDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'data', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Try to update non-existent document + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'update', + 'documentId' => 'non_existent_doc', + 'data' => ['data' => 'Should fail'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); // Operation added + + // Commit should fail + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); // Document not found + + // Test delete non-existent document + $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId2 = $transaction2['body']['$id']; + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId2}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'delete', + 'documentId' => 'non_existent_doc', + 'data' => [] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit should fail + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); // Document not found + } + + /** + * Test createDocument with transactionId via normal route + */ + public function testCreateDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'WriteRoutesTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $columns = [ + ['key' => 'name', 'type' => 'string', 'size' => 256, 'required' => true], + ['key' => 'counter', 'type' => 'integer', 'required' => false, 'min' => 0, 'max' => 10000], + ['key' => 'category', 'type' => 'string', 'size' => 256, 'required' => false], + ['key' => 'data', 'type' => 'string', 'size' => 256, 'required' => false], + ]; + + foreach ($columns as $attr) { + $type = $attr['type']; + unset($attr['type']); + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/{$type}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), $attr); + + $this->assertEquals(202, $response['headers']['status-code']); + } + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Create document via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_from_route', + 'data' => [ + 'name' => 'Created via normal route', + 'counter' => 100, + 'category' => 'test' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_from_route", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Created via normal route', $response['body']['name']); + } + + /** + * Test updateDocument with transactionId via normal route + */ + public function testUpdateDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'UpdateRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_update', + 'data' => [ + 'name' => 'Original name', + 'counter' => 50, + 'category' => 'original' + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Update document via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Updated via normal route', + 'counter' => 150, + 'category' => 'updated' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should still have original values outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Original name', $response['body']['name']); + $this->assertEquals(50, $response['body']['counter']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now have updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated via normal route', $response['body']['name']); + $this->assertEquals(150, $response['body']['counter']); + } + + /** + * Test upsertDocument with transactionId via normal route + */ + public function testUpsertDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'UpsertRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Upsert document (create) via normal route with transactionId + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Created by upsert', + 'counter' => 25 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Document should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Upsert same document (update) in same transaction + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_upsert', + 'data' => [ + 'name' => 'Updated by upsert', + 'counter' => 75 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); // Upsert in transaction returns 201 + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should now exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Updated by upsert', $response['body']['name']); + $this->assertEquals(75, $response['body']['counter']); + } + + /** + * Test deleteDocument with transactionId via normal route + */ + public function testDeleteDocument(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'DeleteRouteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create document outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_to_delete', + 'data' => ['name' => 'Will be deleted'] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Delete document via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Document should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Document should no longer exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + /** + * Test bulkCreate with transactionId via normal route + */ + public function testBulkCreate(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkCreateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk create via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_create_1', + 'name' => 'Bulk created 1', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_2', + 'name' => 'Bulk created 2', + 'category' => 'bulk_created' + ], + [ + '$id' => 'bulk_create_3', + 'name' => 'Bulk created 3', + 'category' => 'bulk_created' + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk operations return 200 + + // Documents should not exist outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + + // Individual document check + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_create_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_created'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Verify individual documents + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_create_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("Bulk created {$i}", $response['body']['name']); + $this->assertEquals('bulk_created', $response['body']['category']); + } + } + + /** + * Test bulkUpdate with transactionId via normal route + */ + public function testBulkUpdate(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpdateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_update_' . $i, + 'data' => [ + 'name' => 'Bulk doc ' . $i, + 'category' => 'bulk_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk update via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()], + 'data' => ['category' => 'bulk_updated'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should still have original category outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now have updated category + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_updated'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + } + + /** + * Test bulkUpsert with transactionId via normal route + */ + public function testBulkUpsert(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpsertTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // Create one document outside transaction + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_upsert_existing', + 'data' => [ + 'name' => 'Existing doc', + 'counter' => 10 + ] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk upsert via normal route with transactionId (updates existing, creates new) + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documents' => [ + [ + '$id' => 'bulk_upsert_existing', + 'name' => 'Updated existing', + 'counter' => 20 + ], + [ + '$id' => 'bulk_upsert_new', + 'name' => 'New doc', + 'counter' => 30 + ] + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Original document should be unchanged, new document shouldn't exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Existing doc', $response['body']['name']); + $this->assertEquals(10, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Check both documents exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_existing", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Updated existing', $response['body']['name']); + $this->assertEquals(20, $response['body']['counter']); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_new", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('New doc', $response['body']['name']); + $this->assertEquals(30, $response['body']['counter']); + } + + /** + * Test bulkDelete with transactionId via normal route + */ + public function testBulkDelete(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkDeleteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + // Create documents for bulk testing + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'bulk_delete_' . $i, + 'data' => [ + 'name' => 'Delete doc ' . $i, + 'category' => 'bulk_delete_test' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Bulk delete via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); // Bulk delete with transaction returns 200 + + // Documents should still exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(3, $response['body']['total']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Documents should now be deleted + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] + ]); + + $this->assertEquals(0, $response['body']['total']); + } + + /** + * Test multiple single route operations in one transaction + */ + public function testMixedSingleOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'MultipleSingleRoutesDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'required' => false, + 'min' => 1, + 'max' => 10, + ]); + + sleep(3); + + // Create an existing document outside transaction for testing + $existingDoc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_doc', + 'data' => [ + 'name' => 'Existing Document', + 'status' => 'active', + 'priority' => 5 + ] + ]); + + $this->assertEquals(201, $existingDoc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + $this->assertEquals(201, $transaction['headers']['status-code']); + + // 1. Create new document via normal route with transactionId + $response1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_1', + 'data' => [ + 'name' => 'New Document 1', + 'status' => 'pending', + 'priority' => 1 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response1['headers']['status-code']); + + // 2. Create another document via normal route with transactionId + $response2 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'new_doc_2', + 'data' => [ + 'name' => 'New Document 2', + 'status' => 'pending', + 'priority' => 2 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response2['headers']['status-code']); + + // 3. Update existing document via normal route with transactionId + $response3 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'updated', + 'priority' => 10 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response3['headers']['status-code']); + + // 4. Update the first new document (created in same transaction) + $response4 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'active', + 'priority' => 8 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response4['headers']['status-code']); + + // 5. Delete the second new document (created in same transaction) + $response5 = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'transactionId' => $transactionId + ]); + + $this->assertEquals(204, $response5['headers']['status-code']); + + // 6. Upsert a new document via normal route with transactionId + $response6 = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'upserted_doc', + 'data' => [ + 'name' => 'Upserted Document', + 'status' => 'new', + 'priority' => 3 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response6['headers']['status-code']); + + // Check transaction has correct number of operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(200, $txnDetails['headers']['status-code']); + $this->assertEquals(6, $txnDetails['body']['operations']); // 6 operations total + + // Verify nothing exists outside transaction yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Existing doc should still have original values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(5, $response['body']['priority']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('committed', $response['body']['status']); + + // Verify final state after commit + // new_doc_1 should exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('New Document 1', $response['body']['name']); + $this->assertEquals('active', $response['body']['status']); + $this->assertEquals(8, $response['body']['priority']); + + // new_doc_2 should not exist (was deleted in transaction) + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // existing_doc should have updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('updated', $response['body']['status']); + $this->assertEquals(10, $response['body']['priority']); + + // upserted_doc should exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Upserted Document', $response['body']['name']); + $this->assertEquals('new', $response['body']['status']); + $this->assertEquals(3, $response['body']['priority']); + + // Verify total document count + $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(3, $documents['body']['total']); // existing_doc, new_doc_1, upserted_doc + } + + /** + * Test mixed operations with transactions + */ + public function testMixedOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'MixedOpsTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create column + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add operation via Operations\Add endpoint + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'create', + 'documentId' => 'mixed_doc1', + 'data' => ['name' => 'Via Operations Add'] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['operations']); + + // Add operation via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'mixed_doc2', + 'data' => ['name' => 'Via normal route'], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Check transaction now has 2 operations + $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(2, $txnDetails['body']['operations']); + + // Both documents shouldn't exist yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Both documents should now exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via Operations Add', $response['body']['name']); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Via normal route', $response['body']['name']); + } + + /** + * Test bulk update with queries that should match documents created in the same transaction + */ + public function testBulkUpdateWithTransactionAwareQueries(): void + { + // Create database and collection + $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' => 'BulkTxnAwareDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'age', + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for columns to be created + + // Create some existing documents + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'age' => 20 + $i, + 'status' => 'inactive' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Create new documents with age > 25 in transaction + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'txn_doc_1', + 'data' => [ + 'name' => 'Transaction Doc 1', + 'age' => 30, + 'status' => 'inactive' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'txn_doc_2', + 'data' => [ + 'name' => 'Transaction Doc 2', + 'age' => 35, + 'status' => 'inactive' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Step 2: Bulk update all documents with age > 25 to have status 'active' + // This should match both existing_3 (age=23 doesn't match, age=24 doesn't match, but existing documents have age 21,22,23) + // Wait, let me fix the ages - existing docs have ages 21, 22, 23, so only txn docs should match + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'active' + ], + 'queries' => [Query::greaterThan('age', 25)->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify that documents created in the transaction were updated by the bulk update + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/txn_doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/txn_doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); + + // Verify existing documents were not affected + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('inactive', $response['body']['status'], "Existing document {$i} should remain inactive (age <= 25)"); + } + } + + /** + * Test bulk update with queries that should match documents updated in the same transaction + */ + public function testBulkUpdateMatchingUpdatedDocuments(): void + { + // Create database and collection + $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' => 'BulkUpdateTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for columns to be created + + // Create existing documents + for ($i = 1; $i <= 4; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_' . $i, + 'data' => [ + 'name' => 'Document ' . $i, + 'category' => 'normal', + 'priority' => 'low' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Update some documents to have category 'special' in transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'category' => 'special' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'category' => 'special' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Step 2: Bulk update all documents with category 'special' to have priority 'high' + // This should match the documents we just updated in the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'priority' => 'high' + ], + 'queries' => [Query::equal('category', ['special'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify that the updated documents were matched by bulk update + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('special', $response['body']['category']); + $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('special', $response['body']['category']); + $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); + + // Verify other documents were not affected + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_3", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('normal', $response['body']['category']); + $this->assertEquals('low', $response['body']['priority']); + } + + /** + * Test bulk delete with queries that should match documents created in the same transaction + */ + public function testBulkDeleteMatchingCreatedDocuments(): void + { + // Create database and collection + $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' => 'BulkDeleteTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'type', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for columns to be created + + // Create existing documents + for ($i = 1; $i <= 3; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'existing_' . $i, + 'data' => [ + 'name' => 'Existing ' . $i, + 'type' => 'permanent' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Create temporary documents in transaction + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'temp_1', + 'data' => [ + 'name' => 'Temporary 1', + 'type' => 'temporary' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'temp_2', + 'data' => [ + 'name' => 'Temporary 2', + 'type' => 'temporary' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Step 2: Bulk delete all documents with type 'temporary' + // This should delete the documents we just created in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('type', ['temporary'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify temporary documents were deleted (should not exist) + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/temp_1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/temp_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + + // Verify existing documents were not affected + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code'], "Permanent document {$i} should still exist"); + $this->assertEquals('permanent', $response['body']['type']); + } + } + + /** + * Test bulk delete with queries that should match documents updated in the same transaction + */ + public function testBulkDeleteMatchingUpdatedDocuments(): void + { + // Create database and collection + $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' => 'BulkDeleteUpdateTxnDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $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'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'TestCollection', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => true, + ]); + + sleep(3); // Wait for columns to be created + + // Create existing documents + for ($i = 1; $i <= 5; $i++) { + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => 'doc_' . $i, + 'data' => [ + 'name' => 'Document ' . $i, + 'status' => 'active' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Step 1: Mark some documents for deletion by updating their status + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'marked_for_deletion' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_4", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'status' => 'marked_for_deletion' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Step 2: Bulk delete all documents with status 'marked_for_deletion' + // This should delete the documents we just updated in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'queries' => [Query::equal('status', ['marked_for_deletion'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify marked documents were deleted + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); + + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_4", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); + + // Verify other documents still exist + foreach ([1, 3, 5] as $i) { + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_{$i}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code'], "Document {$i} should still exist"); + $this->assertEquals('active', $response['body']['status']); + } + } +} From 28a2b577e74af4c945ef26f80080c0b987c822df Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 16:36:09 +1200 Subject: [PATCH 081/145] Update lock --- composer.json | 2 +- composer.lock | 72 +++++++++---------- .../Modules/Databases/Services/Http.php | 1 - 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/composer.json b/composer.json index 9042cb088f..1dc7288441 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "2.*", + "utopia-php/database": "1.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dns": "0.3.*", diff --git a/composer.lock b/composer.lock index e134913475..f82ba2aced 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3565fcc2471b5d18a159b6da1c8fad31", + "content-hash": "7553e976312b0423cc31544abb91caec", "packages": [ { "name": "adhocore/jwt", @@ -3296,16 +3296,16 @@ }, { "name": "utopia-php/abuse", - "version": "1.0.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a" + "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd591568791556d246d901d6aaf9935ab02c3f9a", - "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/c5e2232033b507a07f72180dc56d37e1872ee7be", + "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be", "shasum": "" }, "require": { @@ -3313,7 +3313,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "2.*" + "utopia-php/database": "1.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3341,9 +3341,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.0.1" + "source": "https://github.com/utopia-php/abuse/tree/1.0.0" }, - "time": "2025-09-04T12:46:54+00:00" + "time": "2025-08-13T09:12:54+00:00" }, { "name": "utopia-php/analytics", @@ -3393,21 +3393,21 @@ }, { "name": "utopia-php/audit", - "version": "1.0.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22" + "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", - "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/c0ed75f4d068f1f6c2e7149a909490d4214e72bb", + "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "2.*" + "utopia-php/database": "1.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3434,9 +3434,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/1.0.1" + "source": "https://github.com/utopia-php/audit/tree/1.0.0" }, - "time": "2025-09-04T12:46:43+00:00" + "time": "2025-08-13T09:09:00+00:00" }, { "name": "utopia-php/cache", @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "2.0.0", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5" + "reference": "1bac5c66567cd0718b4c133c8550b3c6076ff908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e4a03ba543abc4e436ec1b450750a14bd36011d5", - "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5", + "url": "https://api.github.com/repos/utopia-php/database/zipball/1bac5c66567cd0718b4c133c8550b3c6076ff908", + "reference": "1bac5c66567cd0718b4c133c8550b3c6076ff908", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/2.0.0" + "source": "https://github.com/utopia-php/database/tree/1.4.5" }, - "time": "2025-09-04T12:36:53+00:00" + "time": "2025-09-11T03:10:29+00:00" }, { "name": "utopia-php/detector", @@ -4190,16 +4190,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.2", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "eb60a61934be1d6f2f4fdabd9903a841ba078bc9" + "reference": "c42935a6a4ee3701c68d24244e82ecb39e945ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/eb60a61934be1d6f2f4fdabd9903a841ba078bc9", - "reference": "eb60a61934be1d6f2f4fdabd9903a841ba078bc9", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c42935a6a4ee3701c68d24244e82ecb39e945ec4", + "reference": "c42935a6a4ee3701c68d24244e82ecb39e945ec4", "shasum": "" }, "require": { @@ -4207,7 +4207,7 @@ "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", - "utopia-php/database": "2.*", + "utopia-php/database": "1.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -4240,9 +4240,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.2" + "source": "https://github.com/utopia-php/migration/tree/1.1.1" }, - "time": "2025-09-04T12:47:05+00:00" + "time": "2025-09-10T06:17:20+00:00" }, { "name": "utopia-php/orchestration", @@ -5007,16 +5007,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.3.2", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "375a6c9b168db6fdf58fbe0d49d2261d80700b4a" + "reference": "d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/375a6c9b168db6fdf58fbe0d49d2261d80700b4a", - "reference": "375a6c9b168db6fdf58fbe0d49d2261d80700b4a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac", + "reference": "d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac", "shasum": "" }, "require": { @@ -5052,9 +5052,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.3.2" + "source": "https://github.com/appwrite/sdk-generator/tree/1.3.4" }, - "time": "2025-09-05T15:50:35+00:00" + "time": "2025-09-08T11:56:04+00:00" }, { "name": "doctrine/annotations", @@ -8512,7 +8512,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8536,5 +8536,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Http.php b/src/Appwrite/Platform/Modules/Databases/Services/Http.php index 86f48c42bb..f683f537bc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php @@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Databases\Services; use Appwrite\Platform\Modules\Databases\Http\Init\Timeout; use Appwrite\Platform\Modules\Databases\Services\Registry\Legacy as LegacyRegistry; use Appwrite\Platform\Modules\Databases\Services\Registry\TablesDB as TablesDBRegistry; -use Appwrite\Platform\Modules\Databases\Services\Registry\Transactions as TransactionsRegistry; use Utopia\Platform\Service; class Http extends Service From f9b92ddead46751765a7f8316aafdc60425da9df Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 16:37:43 +1200 Subject: [PATCH 082/145] Fix namespace --- tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php | 2 +- .../Databases/TablesDB/Transactions/TransactionsTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php index 90282568e4..89e095cb53 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php @@ -1,6 +1,6 @@ Date: Thu, 11 Sep 2025 17:54:55 +1200 Subject: [PATCH 083/145] Legacy DB support --- composer.json | 2 +- composer.lock | 56 +- .../Http/Databases/Collections/Action.php | 4 +- .../Collections/Attributes/Action.php | 4 +- .../Collections/Attributes/Boolean/Create.php | 4 +- .../Collections/Attributes/Boolean/Update.php | 4 +- .../Attributes/Datetime/Create.php | 4 +- .../Attributes/Datetime/Update.php | 4 +- .../Collections/Attributes/Delete.php | 4 +- .../Collections/Attributes/Email/Create.php | 4 +- .../Collections/Attributes/Email/Update.php | 4 +- .../Collections/Attributes/Enum/Create.php | 4 +- .../Collections/Attributes/Enum/Update.php | 4 +- .../Collections/Attributes/Float/Create.php | 4 +- .../Collections/Attributes/Float/Update.php | 4 +- .../Databases/Collections/Attributes/Get.php | 4 +- .../Collections/Attributes/IP/Create.php | 4 +- .../Collections/Attributes/IP/Update.php | 4 +- .../Collections/Attributes/Integer/Create.php | 4 +- .../Collections/Attributes/Integer/Update.php | 4 +- .../Collections/Attributes/Line/Create.php | 4 +- .../Collections/Attributes/Line/Update.php | 4 +- .../Collections/Attributes/Point/Create.php | 4 +- .../Collections/Attributes/Point/Update.php | 4 +- .../Collections/Attributes/Polygon/Create.php | 4 +- .../Collections/Attributes/Polygon/Update.php | 4 +- .../Attributes/Relationship/Create.php | 4 +- .../Attributes/Relationship/Update.php | 4 +- .../Collections/Attributes/String/Create.php | 6 +- .../Collections/Attributes/String/Update.php | 4 +- .../Collections/Attributes/URL/Create.php | 4 +- .../Collections/Attributes/URL/Update.php | 4 +- .../Collections/Attributes/XList.php | 6 +- .../Http/Databases/Collections/Create.php | 2 +- .../Http/Databases/Collections/Delete.php | 2 +- .../Collections/Documents/Action.php | 4 +- .../Documents/Attribute/Decrement.php | 8 +- .../Documents/Attribute/Increment.php | 8 +- .../Collections/Documents/Bulk/Delete.php | 10 +- .../Collections/Documents/Bulk/Update.php | 10 +- .../Collections/Documents/Bulk/Upsert.php | 10 +- .../Collections/Documents/Create.php | 20 +- .../Collections/Documents/Delete.php | 4 +- .../Databases/Collections/Documents/Get.php | 4 +- .../Collections/Documents/Logs/XList.php | 2 +- .../Collections/Documents/Update.php | 4 +- .../Collections/Documents/Upsert.php | 4 +- .../Databases/Collections/Documents/XList.php | 6 +- .../Http/Databases/Collections/Get.php | 2 +- .../Databases/Collections/Indexes/Action.php | 4 +- .../Databases/Collections/Indexes/Create.php | 4 +- .../Databases/Collections/Indexes/Delete.php | 4 +- .../Databases/Collections/Indexes/Get.php | 4 +- .../Databases/Collections/Indexes/XList.php | 4 +- .../Http/Databases/Collections/Logs/XList.php | 2 +- .../Http/Databases/Collections/Update.php | 2 +- .../Http/Databases/Collections/XList.php | 4 +- .../Http/Databases/Transactions/Action.php | 67 ++ .../Http/Databases/Transactions/Create.php | 3 +- .../Http/Databases/Transactions/Delete.php | 3 +- .../Http/Databases/Transactions/Get.php | 3 +- .../Transactions/Operations/Create.php | 10 +- .../Http/Databases/Transactions/Update.php | 16 +- .../Http/Databases/Transactions/XList.php | 3 +- .../Tables/Columns/Boolean/Create.php | 4 +- .../Tables/Columns/Boolean/Update.php | 4 +- .../Tables/Columns/Datetime/Create.php | 4 +- .../Tables/Columns/Datetime/Update.php | 4 +- .../Http/TablesDB/Tables/Columns/Delete.php | 4 +- .../TablesDB/Tables/Columns/Email/Create.php | 4 +- .../TablesDB/Tables/Columns/Email/Update.php | 4 +- .../TablesDB/Tables/Columns/Enum/Create.php | 4 +- .../TablesDB/Tables/Columns/Enum/Update.php | 4 +- .../TablesDB/Tables/Columns/Float/Create.php | 4 +- .../TablesDB/Tables/Columns/Float/Update.php | 4 +- .../Http/TablesDB/Tables/Columns/Get.php | 4 +- .../TablesDB/Tables/Columns/IP/Create.php | 4 +- .../TablesDB/Tables/Columns/IP/Update.php | 4 +- .../Tables/Columns/Integer/Create.php | 4 +- .../Tables/Columns/Integer/Update.php | 4 +- .../TablesDB/Tables/Columns/Line/Create.php | 4 +- .../TablesDB/Tables/Columns/Line/Update.php | 4 +- .../TablesDB/Tables/Columns/Point/Create.php | 4 +- .../TablesDB/Tables/Columns/Point/Update.php | 4 +- .../Tables/Columns/Polygon/Create.php | 4 +- .../Tables/Columns/Polygon/Update.php | 4 +- .../Tables/Columns/Relationship/Create.php | 4 +- .../Tables/Columns/Relationship/Update.php | 4 +- .../TablesDB/Tables/Columns/String/Create.php | 4 +- .../TablesDB/Tables/Columns/String/Update.php | 4 +- .../TablesDB/Tables/Columns/URL/Create.php | 4 +- .../TablesDB/Tables/Columns/URL/Update.php | 4 +- .../Http/TablesDB/Tables/Columns/XList.php | 4 +- .../Databases/Http/TablesDB/Tables/Create.php | 2 +- .../Databases/Http/TablesDB/Tables/Delete.php | 2 +- .../Databases/Http/TablesDB/Tables/Get.php | 2 +- .../Http/TablesDB/Tables/Indexes/Create.php | 4 +- .../Http/TablesDB/Tables/Indexes/Delete.php | 4 +- .../Http/TablesDB/Tables/Indexes/Get.php | 4 +- .../Http/TablesDB/Tables/Indexes/XList.php | 4 +- .../Http/TablesDB/Tables/Logs/XList.php | 4 +- .../Http/TablesDB/Tables/Rows/Bulk/Delete.php | 4 +- .../Http/TablesDB/Tables/Rows/Bulk/Update.php | 4 +- .../Http/TablesDB/Tables/Rows/Bulk/Upsert.php | 4 +- .../TablesDB/Tables/Rows/Column/Decrement.php | 4 +- .../TablesDB/Tables/Rows/Column/Increment.php | 4 +- .../Http/TablesDB/Tables/Rows/Create.php | 8 +- .../Http/TablesDB/Tables/Rows/Delete.php | 5 +- .../Http/TablesDB/Tables/Rows/Get.php | 6 +- .../Http/TablesDB/Tables/Rows/Logs/XList.php | 2 +- .../Http/TablesDB/Tables/Rows/Update.php | 5 +- .../Http/TablesDB/Tables/Rows/Upsert.php | 5 +- .../Http/TablesDB/Tables/Rows/XList.php | 6 +- .../Databases/Http/TablesDB/Tables/Update.php | 2 +- .../Http/TablesDB/Tables/Usage/Get.php | 2 +- .../Databases/Http/TablesDB/Tables/XList.php | 2 +- .../Databases/Services/Registry/Legacy.php | 1 + .../TablesDB/Transactions/ACIDTest.php | 166 ++-- .../Transactions/TransactionsTest.php | 802 +++++++++--------- 119 files changed, 831 insertions(+), 762 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php diff --git a/composer.json b/composer.json index 1dc7288441..9042cb088f 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "1.*", + "utopia-php/database": "2.*", "utopia-php/detector": "0.1.*", "utopia-php/domains": "0.8.*", "utopia-php/dns": "0.3.*", diff --git a/composer.lock b/composer.lock index f82ba2aced..a1c8862168 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7553e976312b0423cc31544abb91caec", + "content-hash": "773efb29b9b584b1249790e0016c823a", "packages": [ { "name": "adhocore/jwt", @@ -3296,16 +3296,16 @@ }, { "name": "utopia-php/abuse", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be" + "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/c5e2232033b507a07f72180dc56d37e1872ee7be", - "reference": "c5e2232033b507a07f72180dc56d37e1872ee7be", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/cd591568791556d246d901d6aaf9935ab02c3f9a", + "reference": "cd591568791556d246d901d6aaf9935ab02c3f9a", "shasum": "" }, "require": { @@ -3313,7 +3313,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "1.*" + "utopia-php/database": "2.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3341,9 +3341,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.0.0" + "source": "https://github.com/utopia-php/abuse/tree/1.0.1" }, - "time": "2025-08-13T09:12:54+00:00" + "time": "2025-09-04T12:46:54+00:00" }, { "name": "utopia-php/analytics", @@ -3393,21 +3393,21 @@ }, { "name": "utopia-php/audit", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb" + "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/c0ed75f4d068f1f6c2e7149a909490d4214e72bb", - "reference": "c0ed75f4d068f1f6c2e7149a909490d4214e72bb", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", + "reference": "5ef26d6a2ab2db7bb86288a1a6ef970307b46f22", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "1.*" + "utopia-php/database": "2.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3434,9 +3434,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/1.0.0" + "source": "https://github.com/utopia-php/audit/tree/1.0.1" }, - "time": "2025-08-13T09:09:00+00:00" + "time": "2025-09-04T12:46:43+00:00" }, { "name": "utopia-php/cache", @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "1.4.5", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "1bac5c66567cd0718b4c133c8550b3c6076ff908" + "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/1bac5c66567cd0718b4c133c8550b3c6076ff908", - "reference": "1bac5c66567cd0718b4c133c8550b3c6076ff908", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e4a03ba543abc4e436ec1b450750a14bd36011d5", + "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.4.5" + "source": "https://github.com/utopia-php/database/tree/2.0.0" }, - "time": "2025-09-11T03:10:29+00:00" + "time": "2025-09-04T12:36:53+00:00" }, { "name": "utopia-php/detector", @@ -4190,16 +4190,16 @@ }, { "name": "utopia-php/migration", - "version": "1.1.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "c42935a6a4ee3701c68d24244e82ecb39e945ec4" + "reference": "6fb6f8f032cd34c3c65728a55d494adeac2ff038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c42935a6a4ee3701c68d24244e82ecb39e945ec4", - "reference": "c42935a6a4ee3701c68d24244e82ecb39e945ec4", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/6fb6f8f032cd34c3c65728a55d494adeac2ff038", + "reference": "6fb6f8f032cd34c3c65728a55d494adeac2ff038", "shasum": "" }, "require": { @@ -4207,7 +4207,7 @@ "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", - "utopia-php/database": "1.*", + "utopia-php/database": "2.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -4240,9 +4240,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.1.1" + "source": "https://github.com/utopia-php/migration/tree/1.1.0" }, - "time": "2025-09-10T06:17:20+00:00" + "time": "2025-09-10T05:45:30+00:00" }, { "name": "utopia-php/orchestration", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php index 0939376c49..b22119fad1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php @@ -53,7 +53,7 @@ abstract class Action extends UtopiaAction /** * Get the SDK group name for the current action. */ - protected function getSdkGroup(): string + protected function getSDKGroup(): string { return $this->isCollectionsAPI() ? 'collections' : 'tables'; } @@ -61,7 +61,7 @@ abstract class Action extends UtopiaAction /** * Get the SDK namespace for the current action. */ - protected function getSdkNamespace(): string + protected function getSDKNamespace(): string { return $this->isCollectionsAPI() ? 'databases' : 'tablesDB'; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index f3903d91a7..ae97d1b7cf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -66,7 +66,7 @@ abstract class Action extends UtopiaAction * * Can be used for XList operations as well! */ - protected function getSdkGroup(): string + protected function getSDKGroup(): string { return $this->isCollectionsAPI() ? 'attributes' : 'columns'; } @@ -74,7 +74,7 @@ abstract class Action extends UtopiaAction /** * Get the SDK namespace for the current action. */ - protected function getSdkNamespace(): string + protected function getSDKNamespace(): string { return $this->isCollectionsAPI() ? 'databases' : 'tablesDB'; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php index 6c4be252a0..6c8e5dcf3d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php @@ -42,8 +42,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-boolean-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php index 752e8c6c59..d4724ea551 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php @@ -42,8 +42,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-boolean-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php index 5d5b8fe4bf..1f2098e7af 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php @@ -43,8 +43,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-datetime-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php index e1d1d40f75..cb4d0d924b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-datetime-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index 8dec7530bf..eb51044323 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -43,8 +43,8 @@ class Delete extends Action ->label('audits.event', 'attribute.delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/delete-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php index bc58ca10af..cbfd66e4d9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php @@ -43,8 +43,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-email-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php index 53300725d7..2446722f7a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-email-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php index cd22e87d1c..98ed83861d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php @@ -45,8 +45,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-enum-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php index 951e43effe..23dc807360 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php @@ -44,8 +44,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-enum-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php index 2a8253c22c..83deb92edc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php @@ -45,8 +45,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-float-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php index 327a766cee..7f295a1a94 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-float-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php index 28ee584778..91fa3582f7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Get.php @@ -47,8 +47,8 @@ class Get extends Action ->label('scope', 'collections.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/get-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php index aa699b4a49..6e6264466c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php @@ -43,8 +43,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-ip-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php index f61cf21732..6cedf10760 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-ip-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php index 81a11b0471..090d63c403 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php @@ -45,8 +45,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-integer-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php index 11cfb24943..b6ae79bd8a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-integer-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php index 4eb3345823..f691fc29cf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php @@ -44,8 +44,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-line-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php index 8fcb867923..52f04ba5bc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-line-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php index 47c88497ba..aae715ba1e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php @@ -44,8 +44,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-point-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php index bffe802927..73964ab461 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-point-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php index 6cc74254ae..6fbbd46d2c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php @@ -44,8 +44,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-polygon-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php index d78bfa8ef0..23eb06f45d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-polygon-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php index a062816329..75471b826c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php @@ -45,8 +45,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-relationship-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php index c19c4ff046..897cbd434f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php @@ -41,8 +41,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-relationship-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php index 592aa8ee93..1527c4d1d9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php @@ -47,8 +47,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-string-attribute.md', auth: [AuthType::KEY], @@ -95,7 +95,7 @@ class Create extends Action array $plan ): void { if (!App::isDevelopment() && $encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSdkGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSdkGroup() . '.'); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSDKGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSDKGroup() . '.'); } if ($encrypt && $size < APP_DATABASE_ENCRYPT_SIZE_MIN) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php index 03efac1430..8614dfb202 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php @@ -45,8 +45,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-string-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php index 39153b3cb8..ce0175966b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php @@ -43,8 +43,8 @@ class Create extends Action ->label('audits.event', 'attribute.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-url-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php index 7ace4d0683..7ba12ad98a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php @@ -43,8 +43,8 @@ class Update extends Action ->label('audits.event', 'attribute.update') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-url-attribute.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php index 1852290f76..6daa180df9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/XList.php @@ -41,8 +41,8 @@ class XList extends Action ->label('scope', 'collections.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/list-attributes.md', auth: [AuthType::KEY], @@ -141,7 +141,7 @@ class XList extends Action $response->dynamic(new Document([ 'total' => $total, - $this->getSdkGroup() => $attributes, + $this->getSDKGroup() => $attributes, ]), $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index e06dae89bb..b810ce602f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -52,7 +52,7 @@ class Create extends Action ->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}') ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-collection.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php index d20570fb43..d124a47289 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php @@ -42,7 +42,7 @@ class Delete extends Action ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/delete-collection.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index d1d0738990..871d1c4cbb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -76,7 +76,7 @@ abstract class Action extends AppwriteAction * * Can be used for XList operations as well! */ - protected function getSdkGroup(): string + protected function getSDKGroup(): string { return $this->isCollectionsAPI() ? 'documents' : 'rows'; } @@ -84,7 +84,7 @@ abstract class Action extends AppwriteAction /** * Get the SDK namespace for the current action. */ - protected function getSdkNamespace(): string + protected function getSDKNamespace(): string { return $this->isCollectionsAPI() ? 'databases' : 'tablesDB'; } 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 39bb5b1f97..cbe0ddceaf 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 @@ -54,8 +54,8 @@ class Decrement extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/decrement-document-attribute.md', auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY], @@ -166,9 +166,9 @@ class Decrement extends Action } catch (NotFoundException) { throw new Exception($this->getStructureNotFoundException()); } catch (LimitException) { - throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min); + throw new Exception($this->getLimitException(), $this->getSDKNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min); } catch (TypeException) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number'); + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSDKNamespace() . ' "' . $attribute . '" is not a number'); } catch (InvalidArgumentException $e) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); } 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 0fa19f8370..22e19c69a5 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 @@ -54,8 +54,8 @@ class Increment extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/increment-document-attribute.md', auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY], @@ -166,9 +166,9 @@ class Increment extends Action } catch (NotFoundException) { throw new Exception($this->getStructureNotFoundException()); } catch (LimitException) { - throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max); + throw new Exception($this->getLimitException(), $this->getSDKNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max); } catch (TypeException) { - throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number'); + throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSDKNamespace() . ' "' . $attribute . '" is not a number'); } catch (InvalidArgumentException $e) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage()); } 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 82f940bb62..57e81c1f3c 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 @@ -51,8 +51,8 @@ class Delete extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/delete-documents.md', auth: [AuthType::ADMIN, AuthType::KEY], @@ -101,7 +101,7 @@ class Delete extends Action ); if ($hasRelationships) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes'); } $originalQueries = $queries; @@ -152,7 +152,7 @@ class Delete extends Action // Return successful response without actually deleting documents $response->dynamic(new Document([ - $this->getSdkGroup() => [], + $this->getSDKGroup() => [], 'total' => 0, // Can't predict how many would be deleted ]), $this->getResponseModel()); return; @@ -187,7 +187,7 @@ class Delete extends Action $response->dynamic(new Document([ 'total' => $modified, - $this->getSdkGroup() => $documents, + $this->getSDKGroup() => $documents, ]), $this->getResponseModel()); $this->triggerBulk( 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 1b9559d4b9..c2d1bb06fc 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 @@ -54,8 +54,8 @@ class Update extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-documents.md', auth: [AuthType::ADMIN, AuthType::KEY], @@ -113,7 +113,7 @@ class Update extends Action ); if ($hasRelationships) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes'); } $originalQueries = $queries; @@ -174,7 +174,7 @@ class Update extends Action // Return successful response without actually updating documents $response->dynamic(new Document([ - $this->getSdkGroup() => [], + $this->getSDKGroup() => [], 'total' => 0, // Can't predict how many would be updated ]), $this->getResponseModel()); return; @@ -214,7 +214,7 @@ class Update extends Action $response->dynamic(new Document([ 'total' => $modified, - $this->getSdkGroup() => $documents + $this->getSDKGroup() => $documents ]), $this->getResponseModel()); $this->triggerBulk( 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 618d640d95..371e0c7360 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 @@ -52,8 +52,8 @@ class Upsert extends Action ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/upsert-documents.md', auth: [AuthType::ADMIN, AuthType::KEY], @@ -103,7 +103,7 @@ class Upsert extends Action ); if ($hasRelationships) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes'); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes'); } foreach ($documents as $key => $document) { @@ -150,7 +150,7 @@ class Upsert extends Action // Return successful response without actually upserting documents $response->dynamic(new Document([ - $this->getSdkGroup() => [], + $this->getSDKGroup() => [], 'total' => \count($documents), ]), $this->getResponseModel()); @@ -192,7 +192,7 @@ class Upsert extends Action $response->dynamic(new Document([ 'total' => $modified, - $this->getSdkGroup() => $upserted + $this->getSDKGroup() => $upserted ]), $this->getResponseModel()); $this->triggerBulk( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index afc5036aa3..7fd29ba6ef 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -63,8 +63,8 @@ class Create extends Action ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), desc: 'Create document', description: '/docs/references/databases/create-document.md', @@ -90,8 +90,8 @@ class Create extends Action ), ), new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: $this->getBulkActionName(self::getName()), desc: 'Create documents', description: '/docs/references/databases/create-documents.md', @@ -148,7 +148,7 @@ class Create extends Action } if (!empty($data) && !empty($documents)) { // Both single and bulk documents provided - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSdkGroup()); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup()); } if (!empty($data) && empty($documentId)) { // Single document provided without document ID @@ -161,12 +161,12 @@ class Create extends Action $documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId'; throw new Exception( Exception::GENERAL_BAD_REQUEST, - "Param \"$documentId\" is not allowed when creating multiple " . $this->getSdkGroup() . ', set "$id" on each instead.' + "Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.' ); } if (!empty($documents) && !empty($permissions)) { // Bulk documents provided with permissions - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSdkGroup() . ', set "$permissions" on each instead'); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead'); } $isBulk = true; @@ -200,7 +200,7 @@ class Create extends Action ); if ($isBulk && $hasRelationships) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSdkNamespace() .' with relationship ' . $this->getStructureContext()); + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() .' with relationship ' . $this->getStructureContext()); } $setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) { @@ -407,7 +407,7 @@ class Create extends Action // Return successful response without actually creating documents if ($isBulk) { $response->dynamic(new Document([ - $this->getSdkGroup() => [], + $this->getSDKGroup() => [], 'total' => \count($documents), ]), $this->getBulkResponseModel()); } else { @@ -468,7 +468,7 @@ class Create extends Action if ($isBulk) { $response->dynamic(new Document([ 'total' => count($documents), - $this->getSdkGroup() => $documents + $this->getSDKGroup() => $documents ]), $this->getBulkResponseModel()); $this->triggerBulk( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 8459e06c43..be73068c06 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -54,8 +54,8 @@ class Delete extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/delete-document.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index cced1440a5..3d8cd9198c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -43,8 +43,8 @@ class Get extends Action ->label('scope', 'documents.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/get-document.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php index 93c8639520..ac7602ff4c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php @@ -48,7 +48,7 @@ class XList extends Action ->label('scope', 'documents.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'logs', name: self::getName(), description: '/docs/references/databases/get-document-logs.md', 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 a2376b44a2..d144ab9194 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 @@ -56,8 +56,8 @@ class Update extends Action ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-document.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], 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 6690e0214f..f93aea88c8 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 @@ -58,8 +58,8 @@ class Upsert extends Action ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/upsert-document.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 7e6931395e..d2f90c0951 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -46,8 +46,8 @@ class XList extends Action ->label('scope', 'documents.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/list-documents.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -206,7 +206,7 @@ class XList extends Action $response->dynamic(new Document([ 'total' => $total, // rows or documents - $this->getSdkGroup() => $documents, + $this->getSDKGroup() => $documents, ]), $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php index af4b6bf733..89739570c7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Get.php @@ -37,7 +37,7 @@ class Get extends Action ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/get-collection.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php index f136fdd29b..400d716e41 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Action.php @@ -52,7 +52,7 @@ abstract class Action extends UtopiaAction /** * Get the SDK group name for the current action. */ - final protected function getSdkGroup(): string + final protected function getSDKGroup(): string { return 'indexes'; } @@ -60,7 +60,7 @@ abstract class Action extends UtopiaAction /** * Get the SDK namespace for the current action. */ - final protected function getSdkNamespace(): string + final protected function getSDKNamespace(): string { return $this->isCollectionsAPI() ? 'databases' : 'tablesDB'; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index 733259f091..0c6ef8bb23 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -51,8 +51,8 @@ class Create extends Action ->label('audits.event', 'index.create') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/create-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index 9ba4c98046..2bccfdfb52 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -46,8 +46,8 @@ class Delete extends Action ->label('audits.event', 'index.delete') ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/delete-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php index 9e05cc79c7..3d118d1922 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Get.php @@ -37,8 +37,8 @@ class Get extends Action ->label('scope', 'collections.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/get-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php index c0aa9457e7..60e52f883a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php @@ -42,8 +42,8 @@ class XList extends Action ->label('scope', 'collections.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/list-indexes.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php index 1309793234..fa95d375c1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php @@ -49,7 +49,7 @@ class XList extends Action ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/get-collection-logs.md', auth: [AuthType::ADMIN], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php index 5bf740d7d1..49870002ce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php @@ -45,7 +45,7 @@ class Update extends Action ->label('audits.resource', 'database/{request.databaseId}/collections/{request.collectionId}') ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/update-collection.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php index f2f7a2233a..b4cc5470d9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php @@ -44,7 +44,7 @@ class XList extends Action ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', - group: $this->getSdkGroup(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/list-collections.md', auth: [AuthType::KEY], @@ -121,7 +121,7 @@ class XList extends Action $response->dynamic(new Document([ 'total' => $total, - $this->getSdkGroup() => $collections, + $this->getSDKGroup() => $collections, ]), $this->getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php new file mode 100644 index 0000000000..512deb8fc8 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php @@ -0,0 +1,67 @@ +context = TABLES; + } + return parent::setHttpPath($path); + } + + /** + * Get the current API context. + */ + protected function getContext(): string + { + return $this->context; + } + + /** + * Determine if the current action is for the Collections API. + */ + protected function isCollectionsAPI(): bool + { + return $this->getContext() === COLLECTIONS; + } + + /** + * Get the key used in event parameters (e.g., 'collectionId' or 'tableId'). + */ + protected function getGroupId(): string + { + return $this->getContext() . 'Id'; + } + + /** + * Get the resource type for the current action (either 'document' or 'row'). + */ + protected function getResource(): string + { + return $this->isCollectionsAPI() ? 'document' : 'row'; + } + + /** + * Get the resource ID key for the current action. + */ + protected function getResourceId(): string + { + return $this->getResource() . 'Id'; + } + + protected function getAttributeKey(): string + { + return $this->isCollectionsAPI() ? 'attribute' : 'column'; + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index d2f114a888..77589e848a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; -use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -19,7 +18,7 @@ class Create extends Action { public static function getName(): string { - return 'createTransaction'; + return 'createDatabasesTransaction'; } protected function getResponseModel(): string diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php index 77dde44bf6..81315498f6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Extend\Exception; -use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -18,7 +17,7 @@ class Delete extends Action { public static function getName(): string { - return 'deleteTransaction'; + return 'deleteDatabasesTransaction'; } protected function getResponseModel(): string diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php index 3c7eec7741..6afe28d626 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Extend\Exception; -use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -17,7 +16,7 @@ class Get extends Action { public static function getName(): string { - return 'getTransaction'; + return 'getDatabasesTransaction'; } protected function getResponseModel(): string diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 8216d96d5d..58505591ff 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations; use Appwrite\Extend\Exception; -use Appwrite\Platform\Action; +use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -21,7 +21,7 @@ class Create extends Action { public static function getName(): string { - return 'createOperations'; + return 'createDatabasesTransactionOperations'; } protected function getResponseModel(): string @@ -84,7 +84,9 @@ class Create extends Action throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $collections[$operation['collectionId']] ??= $dbForProject->getDocument('database_' . $database->getSequence(), $operation['collectionId']); + $collection = $collections[$operation[$this->getGroupId()]] ??= + $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()]); + if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -94,7 +96,7 @@ class Create extends Action 'databaseInternalId' => $database->getSequence(), 'collectionInternalId' => $collection->getSequence(), 'transactionInternalId' => $transaction->getSequence(), - 'documentId' => $operation['documentId'] ?? null, + 'documentId' => $operation[$this->getResourceId()] ?? null, 'action' => $operation['action'], 'data' => $operation['data'] ?? [], ]); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index e854306a47..3e504822f6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Event\Delete; use Appwrite\Extend\Exception; -use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -27,7 +26,7 @@ class Update extends Action { public static function getName(): string { - return 'updateTransaction'; + return 'updateDatabasesTransaction'; } protected function getResponseModel(): string @@ -73,11 +72,10 @@ class Update extends Action * @param bool $rollback * @param UtopiaResponse $response * @param Database $dbForProject - * @param \Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Delete $queueForDeletes + * @param Delete $queueForDeletes * @return void * @throws ConflictException * @throws Exception - * @throws \DateMalformedStringException * @throws \Throwable * @throws \Utopia\Database\Exception * @throws Authorization @@ -357,7 +355,7 @@ class Update extends Action $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( collection: $collectionId, id: $documentId, - attribute: $data['attribute'], + attribute: $data[$this->getAttributeKey()], value: $data['value'] ?? 1, max: $data['max'] ?? null ); @@ -369,7 +367,7 @@ class Update extends Action $dbForProject->increaseDocumentAttribute( collection: $collectionId, id: $documentId, - attribute: $data['attribute'], + attribute: $data[$this->getAttributeKey()], value: $data['value'] ?? 1, max: $data['max'] ?? null ); @@ -394,7 +392,7 @@ class Update extends Action $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( collection: $collectionId, id: $documentId, - attribute: $data['attribute'], + attribute: $data[$this->getAttributeKey()], value: $data['value'] ?? 1, min: $data['min'] ?? null ); @@ -406,7 +404,7 @@ class Update extends Action $dbForProject->decreaseDocumentAttribute( collection: $collectionId, id: $documentId, - attribute: $data['attribute'], + attribute: $data[$this->getAttributeKey()], value: $data['value'] ?? 1, min: $data['min'] ?? null ); @@ -447,8 +445,6 @@ class Update extends Action \DateTime $createdAt, array &$state ): void { - \var_dump($data); - $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->updateDocuments( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php index e3bd8144a9..b13ca8c52b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Extend\Exception; -use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -20,7 +19,7 @@ class XList extends Action { public static function getName(): string { - return 'listTransactions'; + return 'listDatabasesTransactions'; } protected function getResponseModel(): string diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php index 5222d2e133..8f94ba01ae 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php @@ -37,8 +37,8 @@ class Create extends BooleanCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-boolean-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php index 3c6ef50813..3122227351 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php @@ -39,8 +39,8 @@ class Update extends BooleanUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-boolean-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php index 9598278ffc..db5a3059f1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php @@ -39,8 +39,8 @@ class Create extends DatetimeCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-datetime-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php index d7b5ec2448..151422af75 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php @@ -41,8 +41,8 @@ class Update extends DatetimeUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-datetime-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php index 50a148ce19..26f4ffa898 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php @@ -38,8 +38,8 @@ class Delete extends AttributesDelete ->label('audits.event', 'column.delete') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/delete-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php index e28a216fff..3a8d492e4e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php @@ -38,8 +38,8 @@ class Create extends EmailCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-email-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php index 0fb856acb9..4d32489357 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php @@ -40,8 +40,8 @@ class Update extends EmailUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-email-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php index 5b9d89c7e9..68dc2f8e08 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php @@ -40,8 +40,8 @@ class Create extends EnumCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-enum-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php index 0c00e3f268..3b611a5fde 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php @@ -42,8 +42,8 @@ class Update extends EnumUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-enum-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php index 5967b00196..9fe6987cab 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php @@ -38,8 +38,8 @@ class Create extends FloatCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-float-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php index 9486b3a75c..023e2e834e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php @@ -40,8 +40,8 @@ class Update extends FloatUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-float-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php index d536a7aaf2..c20ef58a39 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Get.php @@ -44,8 +44,8 @@ class Get extends AttributesGet ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/get-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php index 325a9382e5..81ca8da81f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php @@ -38,8 +38,8 @@ class Create extends IPCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-ip-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php index b9e6368307..0db95b0206 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php @@ -40,8 +40,8 @@ class Update extends IPUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-ip-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php index bd6fec3f53..dfca51a6c5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php @@ -38,8 +38,8 @@ class Create extends IntegerCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-integer-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php index be92811d1b..a1568d069b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php @@ -40,8 +40,8 @@ class Update extends IntegerUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-integer-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php index f60d4dd5b8..9f3dc3b01e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php @@ -40,8 +40,8 @@ class Create extends LineCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-line-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php index 19c6df202d..cbf0a50de2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php @@ -41,8 +41,8 @@ class Update extends LineUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-line-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php index 47d97e8077..71e51425d6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php @@ -40,8 +40,8 @@ class Create extends PointCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-point-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php index 2e98bf2cf9..ef3e7f6331 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php @@ -41,8 +41,8 @@ class Update extends PointUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-point-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php index 371d5f8fd5..985c264393 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php @@ -40,8 +40,8 @@ class Create extends PolygonCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-polygon-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php index c5654b77d4..d749335e2c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php @@ -41,8 +41,8 @@ class Update extends PolygonUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-polygon-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php index b6f9663f77..0a6c76d8c5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php @@ -39,8 +39,8 @@ class Create extends RelationshipCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-relationship-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php index 421e11af91..b645454be1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php @@ -39,8 +39,8 @@ class Update extends RelationshipUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-relationship-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php index 14f0c8321e..b6cf5fa8c9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php @@ -40,8 +40,8 @@ class Create extends StringCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-string-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php index fc45557f3b..4f3989ac37 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php @@ -42,8 +42,8 @@ class Update extends StringUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-string-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php index bc53ad5250..99ec36b721 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php @@ -38,8 +38,8 @@ class Create extends URLCreate ->label('audits.event', 'column.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/create-url-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php index 36bd7dc054..51168b0383 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php @@ -40,8 +40,8 @@ class Update extends URLUpdate ->label('audits.event', 'column.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-url-column.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php index ca41bb024d..13bf3257e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/XList.php @@ -33,8 +33,8 @@ class XList extends AttributesXList ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/list-columns.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php index 3965e12a74..4f62200d7c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php @@ -40,7 +40,7 @@ class Create extends CollectionCreate ->label('audits.event', 'table.create') ->label('audits.resource', 'database/{request.databaseId}/table/{response.$id}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'tables', name: self::getName(), description: '/docs/references/tablesdb/create-table.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php index 9bfdf42cef..de068d5b29 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php @@ -36,7 +36,7 @@ class Delete extends CollectionDelete ->label('audits.event', 'table.delete') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'tables', name: self::getName(), description: '/docs/references/tablesdb/delete-table.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php index a7d33478f7..be6ec5d9e7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Get.php @@ -33,7 +33,7 @@ class Get extends CollectionGet ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'tables', name: self::getName(), description: '/docs/references/tablesdb/get-table.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php index a2a5c8b453..5b61a4bacc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php @@ -42,8 +42,8 @@ class Create extends IndexCreate ->label('audits.event', 'index.create') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: 'createIndex', // getName needs to be different from parent action to avoid conflict in path name description: '/docs/references/tablesdb/create-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php index 586bad78f4..9b1583585b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php @@ -41,8 +41,8 @@ class Delete extends IndexDelete ->label('audits.event', 'index.delete') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: 'deleteIndex', // getName needs to be different from parent action to avoid conflict in path name description: '/docs/references/tablesdb/delete-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php index 3f2978b547..313260aaee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Get.php @@ -34,8 +34,8 @@ class Get extends IndexGet ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: 'getIndex', // getName needs to be different from parent action to avoid conflict in path name description: '/docs/references/tablesdb/get-index.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php index c275fd2771..b12cb43f2d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/XList.php @@ -34,8 +34,8 @@ class XList extends IndexXList ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: 'listIndexes', // getName needs to be different from parent action to avoid conflict in path name description: '/docs/references/tablesdb/list-indexes.md', auth: [AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php index 6d386df4f6..0680649544 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Logs/XList.php @@ -30,8 +30,8 @@ class XList extends CollectionLogXList ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/get-table-logs.md', auth: [AuthType::ADMIN], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php index a46ffe6cba..7d4396b785 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php @@ -40,8 +40,8 @@ class Delete extends DocumentsDelete ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/delete-rows.md', auth: [AuthType::ADMIN, AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php index 1fc8ba031b..5005ab4f41 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php @@ -41,8 +41,8 @@ class Update extends DocumentsUpdate ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-rows.md', auth: [AuthType::ADMIN, AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php index 3f1349e2f7..d0a1f08003 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php @@ -41,8 +41,8 @@ class Upsert extends DocumentsUpsert ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/upsert-rows.md', auth: [AuthType::ADMIN, AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php index 06cfdb5150..d42cf5e9eb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php @@ -41,8 +41,8 @@ class Decrement extends DecrementDocumentAttribute ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/decrement-row-column.md', auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php index bddda28b25..c58e16c8e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php @@ -41,8 +41,8 @@ class Increment extends IncrementDocumentAttribute ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/increment-row-column.md', auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php index 88fc3c5096..9742208ed6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php @@ -50,8 +50,8 @@ class Create extends DocumentCreate ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), desc: 'Create row', description: '/docs/references/tablesdb/create-row.md', @@ -73,8 +73,8 @@ class Create extends DocumentCreate ] ), new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: $this->getBulkActionName(self::getName()), desc: 'Create rows', description: '/docs/references/tablesdb/create-rows.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php index 0695573ee1..c45029aaab 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php @@ -45,8 +45,8 @@ class Delete extends DocumentDelete ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/delete-row.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -67,6 +67,7 @@ class Delete extends DocumentDelete ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php index 5704f75d82..11faf443d3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php @@ -35,8 +35,8 @@ class Get extends DocumentGet ->label('scope', ['rows.read', 'documents.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/get-row.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -52,9 +52,11 @@ class Get extends DocumentGet ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/tablesdb#tablesDBCreate).') ->param('rowId', '', new UID(), 'Row ID.') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('transactionState') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php index a80249070b..5f1efa2953 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Logs/XList.php @@ -30,7 +30,7 @@ class XList extends DocumentLogXList ->label('scope', ['rows.read', 'documents.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'logs', name: self::getName(), description: '/docs/references/tablesdb/get-row-logs.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php index 6d60cceb22..cb1d5888cf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php @@ -42,8 +42,8 @@ class Update extends DocumentUpdate ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/update-row.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -66,6 +66,7 @@ class Update extends DocumentUpdate ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php index e16fa67e97..0bc373cc93 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php @@ -43,8 +43,8 @@ class Upsert extends DocumentUpsert ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) ->label('sdk', [ new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/upsert-row.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -69,6 +69,7 @@ class Upsert extends DocumentUpsert ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForStatsUsage') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php index 5d503f1c59..6b7fc699fd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php @@ -35,8 +35,8 @@ class XList extends DocumentXList ->label('scope', ['rows.read', 'documents.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), - group: $this->getSdkGroup(), + namespace: $this->getSDKNamespace(), + group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/tablesdb/list-rows.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], @@ -51,9 +51,11 @@ class XList extends DocumentXList ->param('databaseId', '', new UID(), 'Database ID.') ->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TableDB service [server integration](https://appwrite.io/docs/server/tablesdbdb#tablesdbCreate).') ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true) ->inject('response') ->inject('dbForProject') ->inject('queueForStatsUsage') + ->inject('transactionState') ->callback($this->action(...)); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php index 0fcdf319d2..8424b37a6d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php @@ -39,7 +39,7 @@ class Update extends CollectionUpdate ->label('audits.event', 'table.update') ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}') ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'tables', name: self::getName(), description: '/docs/references/tablesdb/update-table.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php index 87f720e689..0fb44ee94a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Usage/Get.php @@ -34,7 +34,7 @@ class Get extends CollectionUsageGet ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: null, name: self::getName(), description: '/docs/references/tablesdb/get-table-usage.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php index d9a92e41b1..a47a972371 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/XList.php @@ -35,7 +35,7 @@ class XList extends CollectionXList ->label('scope', ['tables.read', 'collections.read']) ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( - namespace: $this->getSdkNamespace(), + namespace: $this->getSDKNamespace(), group: 'tables', name: self::getName(), description: '/docs/references/tablesdb/list-tables.md', diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php index 3286aecf93..7de95da255 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php @@ -78,6 +78,7 @@ use Utopia\Platform\Service; * - Documents * - Attributes * - Indexes + * - Transactions */ class Legacy extends Base { diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php index 89e095cb53..9bf459b19f 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/ACIDTest.php @@ -34,25 +34,25 @@ class ACIDTest extends Scope $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; - // Create collection with unique constraint - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create table with unique constraint + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'AtomicityTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Add unique column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -63,7 +63,7 @@ class ACIDTest extends Scope ]); // Add unique index - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/indexes', array_merge([ + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/indexes', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -75,12 +75,12 @@ class ACIDTest extends Scope sleep(3); - // Create first document outside transaction - $doc1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create first row outside transaction + $doc1 = $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()), [ - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'email' => 'existing@example.com' ] @@ -108,27 +108,27 @@ class ACIDTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'email' => 'newuser@example.com' // This should succeed ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'email' => 'existing@example.com' // This will fail - duplicate ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'email' => 'anotheruser@example.com' // This should not be created due to atomicity ] @@ -148,26 +148,26 @@ class ACIDTest extends Scope ]); if ($response['headers']['status-code'] === 200) { - // If transaction succeeded, all documents should be created - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // If transaction succeeded, all rows should be created + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - // Should have 4 documents total (1 original + 3 from transaction) + // Should have 4 rows total (1 original + 3 from transaction) // But since we have a unique constraint violation, this might fail - $this->assertGreaterThanOrEqual(1, $documents['body']['total']); + $this->assertGreaterThanOrEqual(1, $rows['body']['total']); } else { $this->assertEquals(409, $response['headers']['status-code']); // Conflict error - // Verify NO new documents were created (atomicity) - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify NO new rows were created (atomicity) + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(1, $documents['body']['total']); // Only the original document - $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + $this->assertEquals(1, $rows['body']['total']); // Only the original row + $this->assertEquals('existing@example.com', $rows['body']['rows'][0]['email']); } } @@ -189,25 +189,25 @@ class ACIDTest extends Scope $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; - // Create collection with required fields and constraints - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create table with required fields and constraints + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'ConsistencyTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Add required string column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -218,7 +218,7 @@ class ACIDTest extends Scope ]); // Add integer column with min/max constraints - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([ + $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'] @@ -249,9 +249,9 @@ class ACIDTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'required_field' => 'Valid User', 'age' => 25 // Valid age @@ -259,9 +259,9 @@ class ACIDTest extends Scope ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'required_field' => 'Too Young User', 'age' => 10 // Below minimum - will fail constraint @@ -269,9 +269,9 @@ class ACIDTest extends Scope ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => [ 'required_field' => 'Another Valid User', 'age' => 30 // Valid but should not be created due to transaction failure @@ -293,13 +293,13 @@ class ACIDTest extends Scope $this->assertContains($response['headers']['status-code'], [400, 500], 'Transaction commit should fail due to validation. Response: ' . json_encode($response['body'])); - // Verify no documents were created - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify no rows were created + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(0, $documents['body']['total']); + $this->assertEquals(0, $rows['body']['total']); } /** @@ -320,15 +320,15 @@ class ACIDTest extends Scope $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; - // Create collection - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create table + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'IsolationTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -336,10 +336,10 @@ class ACIDTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Add counter column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([ + $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'] @@ -352,12 +352,12 @@ class ACIDTest extends Scope sleep(2); - // Create initial document with counter - $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create initial row with counter + $doc = $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()), [ - 'documentId' => 'shared_counter', + 'rowId' => 'shared_counter', 'data' => [ 'counter' => 0 ] @@ -396,8 +396,8 @@ class ACIDTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => 'shared_counter', + 'tableId' => $tableId, + 'rowId' => 'shared_counter', 'action' => 'increment', 'data' => [ 'column' => 'counter', @@ -416,8 +416,8 @@ class ACIDTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => 'shared_counter', + 'tableId' => $tableId, + 'rowId' => 'shared_counter', 'action' => 'increment', 'data' => [ 'column' => 'counter', @@ -450,13 +450,13 @@ class ACIDTest extends Scope $this->assertEquals(200, $response2['headers']['status-code']); // Check final value - both increments should be applied - $document = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/shared_counter", array_merge([ + $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/shared_counter", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); // Both increments should be applied: 0 + 10 + 5 = 15 - $this->assertEquals(15, $document['body']['counter']); + $this->assertEquals(15, $row['body']['counter']); } /** @@ -477,15 +477,15 @@ class ACIDTest extends Scope $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; - // Create collection - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create table + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'DurabilityTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -494,10 +494,10 @@ class ACIDTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Add column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -529,27 +529,27 @@ class ACIDTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'durable_doc_1', + 'rowId' => 'durable_doc_1', 'data' => [ 'data' => 'Important data 1' ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'durable_doc_2', + 'rowId' => 'durable_doc_2', 'data' => [ 'data' => 'Important data 2' ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'durable_doc_1', + 'rowId' => 'durable_doc_1', 'data' => [ 'data' => 'Updated important data 1' ] @@ -569,33 +569,33 @@ class ACIDTest extends Scope $this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body'])); $this->assertEquals('committed', $response['body']['status']); - // List all documents to see what was created - $allDocs = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // List all rows to see what was created + $allDocs = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body'])); + $this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created rows. Found: ' . json_encode($allDocs['body'])); - // Verify documents exist and have correct data - $document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + // Verify rows exist and have correct data + $row1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/durable_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(200, $document1['headers']['status-code']); - $this->assertEquals('Updated important data 1', $document1['body']['data']); + $this->assertEquals(200, $row1['headers']['status-code']); + $this->assertEquals('Updated important data 1', $row1['body']['data']); - $document2 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_2", array_merge([ + $row2 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/durable_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(200, $document2['headers']['status-code']); - $this->assertEquals('Important data 2', $document2['body']['data']); + $this->assertEquals(200, $row2['headers']['status-code']); + $this->assertEquals('Important data 2', $row2['body']['data']); // Further update outside transaction to ensure persistence - $update = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + $update = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/durable_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -607,19 +607,19 @@ class ACIDTest extends Scope $this->assertEquals(200, $update['headers']['status-code']); // Verify the update persisted - $document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([ + $row1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/durable_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals('Modified outside transaction', $document1['body']['data']); + $this->assertEquals('Modified outside transaction', $row1['body']['data']); - // List all documents to verify total count - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // List all rows to verify total count + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(2, $documents['body']['total']); + $this->assertEquals(2, $rows['body']['total']); } } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index affe44b662..c19100e7ed 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -121,15 +121,15 @@ class TransactionsTest extends Scope $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; - // Create a collection for testing - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create a table for testing + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TransactionOperationsTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -138,11 +138,11 @@ class TransactionsTest extends Scope ], ]); - $this->assertEquals(201, $collection['headers']['status-code']); - $collectionId = $collection['body']['$id']; + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; // Add columns - $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -166,18 +166,18 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc1', + 'rowId' => 'doc1', 'data' => [ 'name' => 'Test Document 1' ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc2', + 'rowId' => 'doc2', 'data' => [ 'name' => 'Test Document 2' ] @@ -197,9 +197,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'doc1', + 'rowId' => 'doc1', 'data' => [ 'name' => 'Updated Document 1' ] @@ -219,9 +219,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => 'invalid_database', - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] @@ -229,7 +229,7 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); - // Test invalid collection ID + // Test invalid table ID $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -238,9 +238,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => 'invalid_collection', + 'tableId' => 'invalid_table', 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] @@ -267,15 +267,15 @@ class TransactionsTest extends Scope $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; - // Create collection - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create table + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TransactionCommitTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -284,11 +284,11 @@ class TransactionsTest extends Scope ], ]); - $this->assertEquals(201, $collection['headers']['status-code']); - $collectionId = $collection['body']['$id']; + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; // Add columns - $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -320,27 +320,27 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc1', + 'rowId' => 'doc1', 'data' => [ 'name' => 'Test Document 1' ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc2', + 'rowId' => 'doc2', 'data' => [ 'name' => 'Test Document 2' ] ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'doc1', + 'rowId' => 'doc1', 'data' => [ 'name' => 'Updated Document 1' ] @@ -363,18 +363,18 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('committed', $response['body']['status']); - // Verify documents were created - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify rows were created + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(200, $documents['headers']['status-code']); - $this->assertEquals(2, $documents['body']['total']); + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertEquals(2, $rows['body']['total']); // Verify the update was applied $doc1Found = false; - foreach ($documents['body']['documents'] as $doc) { + foreach ($rows['body']['rows'] as $doc) { if ($doc['$id'] === 'doc1') { $this->assertEquals('Updated Document 1', $doc['name']); $doc1Found = true; @@ -422,15 +422,15 @@ class TransactionsTest extends Scope $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; - // Create a collection for rollback test - $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + // Create a table for rollback test + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TransactionRollbackTest', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -439,10 +439,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Add column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([ + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -463,9 +463,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'rollback_doc', + 'rowId' => 'rollback_doc', 'data' => [ 'value' => 'Should not exist' ] @@ -487,14 +487,14 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('rolledBack', $response['body']['status']); - // Verify no documents were created - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify no rows were created + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(200, $documents['headers']['status-code']); - $this->assertEquals(0, $documents['body']['total']); + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertEquals(0, $rows['body']['total']); } /** @@ -502,7 +502,7 @@ class TransactionsTest extends Scope */ public function testTransactionExpiration(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -514,12 +514,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -529,10 +529,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -565,9 +565,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['data' => 'Should expire'] ] ] @@ -598,7 +598,7 @@ class TransactionsTest extends Scope */ public function testTransactionSizeLimit(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -610,20 +610,20 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [Permission::create(Role::any())], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -650,9 +650,9 @@ class TransactionsTest extends Scope for ($i = 0; $i < 50; $i++) { $operations[] = [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc_' . $i, + 'rowId' => 'doc_' . $i, 'data' => ['value' => 'Test ' . $i] ]; } @@ -674,8 +674,8 @@ class TransactionsTest extends Scope for ($i = 50; $i < 100; $i++) { $operations[] = [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, - 'documentId' => 'doc_' . $i, + 'tableId' => $tableId, + 'rowId' => 'doc_' . $i, 'action' => 'create', 'data' => ['value' => 'Test ' . $i] ]; @@ -701,9 +701,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'doc_overflow', + 'rowId' => 'doc_overflow', 'data' => ['value' => 'This should fail'] ] ] @@ -717,7 +717,7 @@ class TransactionsTest extends Scope */ public function testConcurrentTransactionConflicts(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -729,12 +729,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -743,10 +743,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -759,13 +759,13 @@ class TransactionsTest extends Scope sleep(2); - // Create initial document - $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create initial row + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'shared_doc', + 'rowId' => 'shared_doc', 'data' => ['counter' => 100] ]); @@ -787,7 +787,7 @@ class TransactionsTest extends Scope $transactionId1 = $txn1['body']['$id']; $transactionId2 = $txn2['body']['$id']; - // Both transactions try to update the same document + // Both transactions try to update the same row $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId1}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -796,9 +796,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'shared_doc', + 'rowId' => 'shared_doc', 'data' => ['counter' => 200] ] ] @@ -812,9 +812,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'shared_doc', + 'rowId' => 'shared_doc', 'data' => ['counter' => 300] ] ] @@ -842,8 +842,8 @@ class TransactionsTest extends Scope $this->assertEquals(409, $response2['headers']['status-code']); // Conflict - // Verify the document has the value from first transaction - $doc = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/shared_doc", array_merge([ + // Verify the row has the value from first transaction + $doc = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/shared_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -852,11 +852,11 @@ class TransactionsTest extends Scope } /** - * Test deleting a document that's being updated in a transaction + * Test deleting a row that's being updated in a transaction */ public function testDeleteDocumentDuringTransaction(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -868,12 +868,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -883,10 +883,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -898,13 +898,13 @@ class TransactionsTest extends Scope sleep(2); - // Create document - $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create row + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'target_doc', + 'rowId' => 'target_doc', 'data' => ['data' => 'Original'] ]); @@ -928,16 +928,16 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'target_doc', + 'rowId' => 'target_doc', 'data' => ['data' => 'Updated in transaction'] ] ] ]); - // Delete the document outside of transaction - $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/target_doc", array_merge([ + // Delete the row outside of transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/target_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -945,7 +945,7 @@ class TransactionsTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); - // Try to commit transaction - should fail because document no longer exists + // Try to commit transaction - should fail because row no longer exists $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -962,7 +962,7 @@ class TransactionsTest extends Scope */ public function testBulkOperations(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -974,12 +974,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -989,10 +989,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1002,7 +1002,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1014,14 +1014,14 @@ class TransactionsTest extends Scope sleep(3); - // Create some initial documents + // Create some initial rows for ($i = 1; $i <= 5; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'existing_' . $i, + 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'category' => 'old' @@ -1048,7 +1048,7 @@ class TransactionsTest extends Scope // Bulk create [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'bulkCreate', 'data' => [ ['$id' => 'bulk_1', 'name' => 'Bulk 1', 'category' => 'new'], @@ -1059,7 +1059,7 @@ class TransactionsTest extends Scope // Bulk update [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'bulkUpdate', 'data' => [ 'queries' => [Query::equal('category', ['old'])->toString()], @@ -1069,7 +1069,7 @@ class TransactionsTest extends Scope // Bulk delete [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'bulkDelete', 'data' => [ 'queries' => [Query::equal('name', ['Existing 5'])->toString()] @@ -1092,20 +1092,20 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Verify results - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - // Should have 7 documents (5 existing - 1 deleted + 3 new) - $this->assertEquals(7, $documents['body']['total']); + // Should have 7 rows (5 existing - 1 deleted + 3 new) + $this->assertEquals(7, $rows['body']['total']); // Check categories were updated $oldCategoryCount = 0; $updatedCategoryCount = 0; $newCategoryCount = 0; - foreach ($documents['body']['documents'] as $doc) { + foreach ($rows['body']['rows'] as $doc) { switch ($doc['category']) { case 'old': $oldCategoryCount++; @@ -1129,7 +1129,7 @@ class TransactionsTest extends Scope */ public function testPartialFailureRollback(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1141,12 +1141,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -1154,10 +1154,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns with constraints - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1170,7 +1170,7 @@ class TransactionsTest extends Scope sleep(2); // Create unique index on email - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/indexes", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/indexes", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1182,13 +1182,13 @@ class TransactionsTest extends Scope sleep(2); - // Create an existing document - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create an existing row + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['email' => 'existing@example.com'] ]); @@ -1210,30 +1210,30 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['email' => 'valid1@example.com'] // Valid ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['email' => 'valid2@example.com'] // Valid ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['email' => 'existing@example.com'] // Will fail - duplicate ], [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['email' => 'valid3@example.com'] // Would be valid but should rollback ], ] @@ -1252,14 +1252,14 @@ class TransactionsTest extends Scope $this->assertEquals(409, $response['headers']['status-code']); // Conflict due to duplicate - // Verify NO new documents were created (atomicity) - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify NO new rows were created (atomicity) + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(1, $documents['body']['total']); // Only the original document - $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']); + $this->assertEquals(1, $rows['body']['total']); // Only the original row + $this->assertEquals('existing@example.com', $rows['body']['rows'][0]['email']); } /** @@ -1267,7 +1267,7 @@ class TransactionsTest extends Scope */ public function testDoubleCommitRollback(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1279,20 +1279,20 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [Permission::create(Role::any())], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1322,9 +1322,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => ID::unique(), + 'rowId' => ID::unique(), 'data' => ['data' => 'Test'] ] ] @@ -1385,11 +1385,11 @@ class TransactionsTest extends Scope } /** - * Test operations on non-existent documents + * Test operations on non-existent rows */ public function testOperationsOnNonExistentDocuments(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1401,12 +1401,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -1415,10 +1415,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1439,7 +1439,7 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Try to update non-existent document + // Try to update non-existent row $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1448,9 +1448,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'update', - 'documentId' => 'non_existent_doc', + 'rowId' => 'non_existent_doc', 'data' => ['data' => 'Should fail'] ] ] @@ -1469,7 +1469,7 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); // Document not found - // Test delete non-existent document + // Test delete non-existent row $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1486,9 +1486,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'delete', - 'documentId' => 'non_existent_doc', + 'rowId' => 'non_existent_doc', 'data' => [] ] ] @@ -1513,7 +1513,7 @@ class TransactionsTest extends Scope */ public function testCreateDocument(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1525,14 +1525,14 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', - 'documentSecurity' => false, + 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), @@ -1541,7 +1541,7 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns $columns = [ @@ -1555,7 +1555,7 @@ class TransactionsTest extends Scope $type = $attr['type']; unset($attr['type']); - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/{$type}", array_merge([ + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/{$type}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1576,13 +1576,13 @@ class TransactionsTest extends Scope $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; - // Create document via normal route with transactionId - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create row via normal route with transactionId + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_from_route', + 'rowId' => 'doc_from_route', 'data' => [ 'name' => 'Created via normal route', 'counter' => 100, @@ -1594,7 +1594,7 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); // Document should not exist outside transaction yet - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_from_route", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_from_route", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1613,7 +1613,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Document should now exist - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_from_route", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_from_route", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1627,7 +1627,7 @@ class TransactionsTest extends Scope */ public function testUpdateDocument(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1639,12 +1639,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -1653,10 +1653,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1666,7 +1666,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -1677,7 +1677,7 @@ class TransactionsTest extends Scope 'max' => 10000, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1689,13 +1689,13 @@ class TransactionsTest extends Scope sleep(3); - // Create document outside transaction - $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create row outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_to_update', + 'rowId' => 'doc_to_update', 'data' => [ 'name' => 'Original name', 'counter' => 50, @@ -1714,8 +1714,8 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Update document via normal route with transactionId - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + // Update row via normal route with transactionId + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1731,7 +1731,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Document should still have original values outside transaction - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1751,7 +1751,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Document should now have updated values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_update", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1765,7 +1765,7 @@ class TransactionsTest extends Scope */ public function testUpsertDocument(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1777,12 +1777,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -1791,10 +1791,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1804,7 +1804,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -1826,13 +1826,13 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Upsert document (create) via normal route with transactionId - $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + // Upsert row (create) via normal route with transactionId + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_upsert', + 'rowId' => 'doc_upsert', 'data' => [ 'name' => 'Created by upsert', 'counter' => 25 @@ -1843,20 +1843,20 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); // Document should not exist outside transaction yet - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); - // Upsert same document (update) in same transaction - $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + // Upsert same row (update) in same transaction + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_upsert', + 'rowId' => 'doc_upsert', 'data' => [ 'name' => 'Updated by upsert', 'counter' => 75 @@ -1878,7 +1878,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Document should now exist with updated values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_upsert", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1893,7 +1893,7 @@ class TransactionsTest extends Scope */ public function testDeleteDocument(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1905,12 +1905,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -1919,10 +1919,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1934,13 +1934,13 @@ class TransactionsTest extends Scope sleep(2); - // Create document outside transaction - $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create row outside transaction + $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_to_delete', + 'rowId' => 'doc_to_delete', 'data' => ['name' => 'Will be deleted'] ]); @@ -1955,8 +1955,8 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Delete document via normal route with transactionId - $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + // Delete row via normal route with transactionId + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -1967,7 +1967,7 @@ class TransactionsTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); // Document should still exist outside transaction - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1986,7 +1986,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Document should no longer exist - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_to_delete", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -1999,7 +1999,7 @@ class TransactionsTest extends Scope */ public function testBulkCreate(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2011,12 +2011,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2024,10 +2024,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2037,7 +2037,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2059,12 +2059,12 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; // Bulk create via normal route with transactionId - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documents' => [ + 'rows' => [ [ '$id' => 'bulk_create_1', 'name' => 'Bulk created 1', @@ -2087,7 +2087,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Bulk operations return 200 // Documents should not exist outside transaction yet - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2096,8 +2096,8 @@ class TransactionsTest extends Scope $this->assertEquals(0, $response['body']['total']); - // Individual document check - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_create_1", array_merge([ + // Individual row check + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_create_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2116,7 +2116,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Documents should now exist - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2125,9 +2125,9 @@ class TransactionsTest extends Scope $this->assertEquals(3, $response['body']['total']); - // Verify individual documents + // Verify individual rows for ($i = 1; $i <= 3; $i++) { - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_create_{$i}", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_create_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2143,7 +2143,7 @@ class TransactionsTest extends Scope */ public function testBulkUpdate(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2155,12 +2155,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2169,10 +2169,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2182,7 +2182,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2194,14 +2194,14 @@ class TransactionsTest extends Scope sleep(3); - // Create documents for bulk testing + // Create rows for bulk testing for ($i = 1; $i <= 3; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'bulk_update_' . $i, + 'rowId' => 'bulk_update_' . $i, 'data' => [ 'name' => 'Bulk doc ' . $i, 'category' => 'bulk_test' @@ -2219,7 +2219,7 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; // Bulk update via normal route with transactionId - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2232,7 +2232,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Documents should still have original category outside transaction - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2253,7 +2253,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Documents should now have updated category - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2268,7 +2268,7 @@ class TransactionsTest extends Scope */ public function testBulkUpsert(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2280,12 +2280,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2294,10 +2294,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2307,7 +2307,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -2320,13 +2320,13 @@ class TransactionsTest extends Scope sleep(3); - // Create one document outside transaction - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create one row outside transaction + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'bulk_upsert_existing', + 'rowId' => 'bulk_upsert_existing', 'data' => [ 'name' => 'Existing doc', 'counter' => 10 @@ -2343,12 +2343,12 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; // Bulk upsert via normal route with transactionId (updates existing, creates new) - $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documents' => [ + 'rows' => [ [ '$id' => 'bulk_upsert_existing', 'name' => 'Updated existing', @@ -2365,8 +2365,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Original document should be unchanged, new document shouldn't exist outside transaction - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_existing", array_merge([ + // Original row should be unchanged, new row shouldn't exist outside transaction + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_existing", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2374,7 +2374,7 @@ class TransactionsTest extends Scope $this->assertEquals('Existing doc', $response['body']['name']); $this->assertEquals(10, $response['body']['counter']); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_new", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_new", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2392,8 +2392,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Check both documents exist with updated values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_existing", array_merge([ + // Check both rows exist with updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_existing", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2401,7 +2401,7 @@ class TransactionsTest extends Scope $this->assertEquals('Updated existing', $response['body']['name']); $this->assertEquals(20, $response['body']['counter']); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/bulk_upsert_new", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_new", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2415,7 +2415,7 @@ class TransactionsTest extends Scope */ public function testBulkDelete(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2427,12 +2427,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2441,10 +2441,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2454,7 +2454,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2466,14 +2466,14 @@ class TransactionsTest extends Scope sleep(3); - // Create documents for bulk testing + // Create rows for bulk testing for ($i = 1; $i <= 3; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'bulk_delete_' . $i, + 'rowId' => 'bulk_delete_' . $i, 'data' => [ 'name' => 'Delete doc ' . $i, 'category' => 'bulk_delete_test' @@ -2491,7 +2491,7 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; // Bulk delete via normal route with transactionId - $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2503,7 +2503,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Bulk delete with transaction returns 200 // Documents should still exist outside transaction - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2524,7 +2524,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); // Documents should now be deleted - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -2539,7 +2539,7 @@ class TransactionsTest extends Scope */ public function testMixedSingleOperations(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2551,12 +2551,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2566,10 +2566,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2579,7 +2579,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2589,7 +2589,7 @@ class TransactionsTest extends Scope 'required' => false, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -2602,13 +2602,13 @@ class TransactionsTest extends Scope sleep(3); - // Create an existing document outside transaction for testing - $existingDoc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Create an existing row outside transaction for testing + $existingDoc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'existing_doc', + 'rowId' => 'existing_doc', 'data' => [ 'name' => 'Existing Document', 'status' => 'active', @@ -2628,13 +2628,13 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; $this->assertEquals(201, $transaction['headers']['status-code']); - // 1. Create new document via normal route with transactionId - $response1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // 1. Create new row via normal route with transactionId + $response1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'new_doc_1', + 'rowId' => 'new_doc_1', 'data' => [ 'name' => 'New Document 1', 'status' => 'pending', @@ -2645,13 +2645,13 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response1['headers']['status-code']); - // 2. Create another document via normal route with transactionId - $response2 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // 2. Create another row via normal route with transactionId + $response2 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'new_doc_2', + 'rowId' => 'new_doc_2', 'data' => [ 'name' => 'New Document 2', 'status' => 'pending', @@ -2662,8 +2662,8 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response2['headers']['status-code']); - // 3. Update existing document via normal route with transactionId - $response3 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + // 3. Update existing row via normal route with transactionId + $response3 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2677,8 +2677,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response3['headers']['status-code']); - // 4. Update the first new document (created in same transaction) - $response4 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + // 4. Update the first new row (created in same transaction) + $response4 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2692,8 +2692,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response4['headers']['status-code']); - // 5. Delete the second new document (created in same transaction) - $response5 = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_2", array_merge([ + // 5. Delete the second new row (created in same transaction) + $response5 = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2703,13 +2703,13 @@ class TransactionsTest extends Scope $this->assertEquals(204, $response5['headers']['status-code']); - // 6. Upsert a new document via normal route with transactionId - $response6 = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + // 6. Upsert a new row via normal route with transactionId + $response6 = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'upserted_doc', + 'rowId' => 'upserted_doc', 'data' => [ 'name' => 'Upserted Document', 'status' => 'new', @@ -2731,14 +2731,14 @@ class TransactionsTest extends Scope $this->assertEquals(6, $txnDetails['body']['operations']); // 6 operations total // Verify nothing exists outside transaction yet - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2746,7 +2746,7 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); // Existing doc should still have original values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2768,7 +2768,7 @@ class TransactionsTest extends Scope // Verify final state after commit // new_doc_1 should exist with updated values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_1", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2779,7 +2779,7 @@ class TransactionsTest extends Scope $this->assertEquals(8, $response['body']['priority']); // new_doc_2 should not exist (was deleted in transaction) - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/new_doc_2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2787,7 +2787,7 @@ class TransactionsTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); // existing_doc should have updated values - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_doc", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2796,7 +2796,7 @@ class TransactionsTest extends Scope $this->assertEquals(10, $response['body']['priority']); // upserted_doc should exist - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/upserted_doc", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2806,13 +2806,13 @@ class TransactionsTest extends Scope $this->assertEquals('new', $response['body']['status']); $this->assertEquals(3, $response['body']['priority']); - // Verify total document count - $documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Verify total row count + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(3, $documents['body']['total']); // existing_doc, new_doc_1, upserted_doc + $this->assertEquals(3, $rows['body']['total']); // existing_doc, new_doc_1, upserted_doc } /** @@ -2820,7 +2820,7 @@ class TransactionsTest extends Scope */ public function testMixedOperations(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2832,12 +2832,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), @@ -2847,10 +2847,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create column - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -2880,9 +2880,9 @@ class TransactionsTest extends Scope 'operations' => [ [ 'databaseId' => $databaseId, - 'collectionId' => $collectionId, + 'tableId' => $tableId, 'action' => 'create', - 'documentId' => 'mixed_doc1', + 'rowId' => 'mixed_doc1', 'data' => ['name' => 'Via Operations Add'] ] ] @@ -2892,12 +2892,12 @@ class TransactionsTest extends Scope $this->assertEquals(1, $response['body']['operations']); // Add operation via normal route with transactionId - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'mixed_doc2', + 'rowId' => 'mixed_doc2', 'data' => ['name' => 'Via normal route'], 'transactionId' => $transactionId ]); @@ -2913,15 +2913,15 @@ class TransactionsTest extends Scope $this->assertEquals(2, $txnDetails['body']['operations']); - // Both documents shouldn't exist yet - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc1", array_merge([ + // Both rows shouldn't exist yet + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2939,8 +2939,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Both documents should now exist - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc1", array_merge([ + // Both rows should now exist + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2948,7 +2948,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Via Operations Add', $response['body']['name']); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/mixed_doc2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -2958,11 +2958,11 @@ class TransactionsTest extends Scope } /** - * Test bulk update with queries that should match documents created in the same transaction + * Test bulk update with queries that should match rows created in the same transaction */ public function testBulkUpdateWithTransactionAwareQueries(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -2974,12 +2974,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -2989,10 +2989,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3002,7 +3002,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/integer", array_merge([ + $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'] @@ -3011,7 +3011,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3023,14 +3023,14 @@ class TransactionsTest extends Scope sleep(3); // Wait for columns to be created - // Create some existing documents + // Create some existing rows for ($i = 1; $i <= 3; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'existing_' . $i, + 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'age' => 20 + $i, @@ -3048,13 +3048,13 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Step 1: Create new documents with age > 25 in transaction - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Step 1: Create new rows with age > 25 in transaction + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'txn_doc_1', + 'rowId' => 'txn_doc_1', 'data' => [ 'name' => 'Transaction Doc 1', 'age' => 30, @@ -3065,12 +3065,12 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'txn_doc_2', + 'rowId' => 'txn_doc_2', 'data' => [ 'name' => 'Transaction Doc 2', 'age' => 35, @@ -3081,10 +3081,10 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); - // Step 2: Bulk update all documents with age > 25 to have status 'active' - // This should match both existing_3 (age=23 doesn't match, age=24 doesn't match, but existing documents have age 21,22,23) + // Step 2: Bulk update all rows with age > 25 to have status 'active' + // This should match both existing_3 (age=23 doesn't match, age=24 doesn't match, but existing rows have age 21,22,23) // Wait, let me fix the ages - existing docs have ages 21, 22, 23, so only txn docs should match - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3109,8 +3109,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Verify that documents created in the transaction were updated by the bulk update - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/txn_doc_1", array_merge([ + // Verify that rows created in the transaction were updated by the bulk update + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/txn_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -3118,7 +3118,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/txn_doc_2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/txn_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -3126,24 +3126,24 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); - // Verify existing documents were not affected + // Verify existing rows were not affected for ($i = 1; $i <= 3; $i++) { - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_{$i}", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('inactive', $response['body']['status'], "Existing document {$i} should remain inactive (age <= 25)"); + $this->assertEquals('inactive', $response['body']['status'], "Existing row {$i} should remain inactive (age <= 25)"); } } /** - * Test bulk update with queries that should match documents updated in the same transaction + * Test bulk update with queries that should match rows updated in the same transaction */ public function testBulkUpdateMatchingUpdatedDocuments(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -3155,12 +3155,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -3170,10 +3170,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3183,7 +3183,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3193,7 +3193,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3205,14 +3205,14 @@ class TransactionsTest extends Scope sleep(3); // Wait for columns to be created - // Create existing documents + // Create existing rows for ($i = 1; $i <= 4; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_' . $i, + 'rowId' => 'doc_' . $i, 'data' => [ 'name' => 'Document ' . $i, 'category' => 'normal', @@ -3230,8 +3230,8 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Step 1: Update some documents to have category 'special' in transaction - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_1", array_merge([ + // Step 1: Update some rows to have category 'special' in transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3244,7 +3244,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3257,9 +3257,9 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Step 2: Bulk update all documents with category 'special' to have priority 'high' - // This should match the documents we just updated in the transaction - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Step 2: Bulk update all rows with category 'special' to have priority 'high' + // This should match the rows we just updated in the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3284,8 +3284,8 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Verify that the updated documents were matched by bulk update - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_1", array_merge([ + // Verify that the updated rows were matched by bulk update + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -3294,7 +3294,7 @@ class TransactionsTest extends Scope $this->assertEquals('special', $response['body']['category']); $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -3303,8 +3303,8 @@ class TransactionsTest extends Scope $this->assertEquals('special', $response['body']['category']); $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); - // Verify other documents were not affected - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_3", array_merge([ + // Verify other rows were not affected + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_3", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -3315,11 +3315,11 @@ class TransactionsTest extends Scope } /** - * Test bulk delete with queries that should match documents created in the same transaction + * Test bulk delete with queries that should match rows created in the same transaction */ public function testBulkDeleteMatchingCreatedDocuments(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -3331,12 +3331,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -3346,10 +3346,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3359,7 +3359,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3371,14 +3371,14 @@ class TransactionsTest extends Scope sleep(3); // Wait for columns to be created - // Create existing documents + // Create existing rows for ($i = 1; $i <= 3; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'existing_' . $i, + 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'type' => 'permanent' @@ -3395,13 +3395,13 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Step 1: Create temporary documents in transaction - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Step 1: Create temporary rows in transaction + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'temp_1', + 'rowId' => 'temp_1', 'data' => [ 'name' => 'Temporary 1', 'type' => 'temporary' @@ -3411,12 +3411,12 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'temp_2', + 'rowId' => 'temp_2', 'data' => [ 'name' => 'Temporary 2', 'type' => 'temporary' @@ -3426,9 +3426,9 @@ class TransactionsTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); - // Step 2: Bulk delete all documents with type 'temporary' - // This should delete the documents we just created in the transaction - $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Step 2: Bulk delete all rows with type 'temporary' + // This should delete the rows we just created in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3450,39 +3450,39 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Verify temporary documents were deleted (should not exist) - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/temp_1", array_merge([ + // Verify temporary rows were deleted (should not exist) + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/temp_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary row created and deleted in transaction should not exist'); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/temp_2", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/temp_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(404, $response['headers']['status-code'], 'Temporary document created and deleted in transaction should not exist'); + $this->assertEquals(404, $response['headers']['status-code'], 'Temporary row created and deleted in transaction should not exist'); - // Verify existing documents were not affected + // Verify existing rows were not affected for ($i = 1; $i <= 3; $i++) { - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/existing_{$i}", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(200, $response['headers']['status-code'], "Permanent document {$i} should still exist"); + $this->assertEquals(200, $response['headers']['status-code'], "Permanent row {$i} should still exist"); $this->assertEquals('permanent', $response['body']['type']); } } /** - * Test bulk delete with queries that should match documents updated in the same transaction + * Test bulk delete with queries that should match rows updated in the same transaction */ public function testBulkDeleteMatchingUpdatedDocuments(): void { - // Create database and collection + // Create database and table $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -3494,12 +3494,12 @@ class TransactionsTest extends Scope $databaseId = $database['body']['$id']; - $collection = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([ + $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'] ]), [ - 'collectionId' => ID::unique(), + 'tableId' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), @@ -3509,10 +3509,10 @@ class TransactionsTest extends Scope ], ]); - $collectionId = $collection['body']['$id']; + $tableId = $table['body']['$id']; // Create columns - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3522,7 +3522,7 @@ class TransactionsTest extends Scope 'required' => true, ]); - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/columns/string", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3534,14 +3534,14 @@ class TransactionsTest extends Scope sleep(3); // Wait for columns to be created - // Create existing documents + // Create existing rows for ($i = 1; $i <= 5; $i++) { - $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'documentId' => 'doc_' . $i, + 'rowId' => 'doc_' . $i, 'data' => [ 'name' => 'Document ' . $i, 'status' => 'active' @@ -3558,8 +3558,8 @@ class TransactionsTest extends Scope $transactionId = $transaction['body']['$id']; - // Step 1: Mark some documents for deletion by updating their status - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + // Step 1: Mark some rows for deletion by updating their status + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3572,7 +3572,7 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_4", array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_4", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3585,9 +3585,9 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Step 2: Bulk delete all documents with status 'marked_for_deletion' - // This should delete the documents we just updated in the transaction - $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([ + // Step 2: Bulk delete all rows with status 'marked_for_deletion' + // This should delete the rows we just updated in the transaction + $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3609,24 +3609,24 @@ class TransactionsTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - // Verify marked documents were deleted - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_2", array_merge([ + // Verify marked rows were deleted + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_4", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_4", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); - // Verify other documents still exist + // Verify other rows still exist foreach ([1, 3, 5] as $i) { - $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/doc_{$i}", array_merge([ + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); From 8706828c2235fa3863997be9590c33aed991e1af Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 21:13:41 +1200 Subject: [PATCH 084/145] Fix tests --- .../Databases/Http/Databases/Transactions/Update.php | 7 +++++-- .../Databases/TablesDB/Transactions/TransactionsTest.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 3e504822f6..dde39efd90 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -271,7 +271,7 @@ class Update extends Action new Document($data), ); if ($document->isEmpty()) { - throw new ConflictException(''); + throw new NotFoundException(''); } $state[$collectionId][$documentId] = $document; }); @@ -330,7 +330,10 @@ class Update extends Action // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, &$state) { - $dbForProject->deleteDocument($collectionId, $documentId); + $deleted = $dbForProject->deleteDocument($collectionId, $documentId); + if (!$deleted) { + throw new NotFoundException(''); + } if (isset($state[$collectionId][$documentId])) { unset($state[$collectionId][$documentId]); } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index c19100e7ed..9ec5994903 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -954,7 +954,7 @@ class TransactionsTest extends Scope 'commit' => true ]); - $this->assertEquals(409, $response['headers']['status-code']); // Conflict + $this->assertEquals(404, $response['headers']['status-code']); } /** From be81c73c4076ee48889fd215f20e740b9b382b98 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 21:30:07 +1200 Subject: [PATCH 085/145] Update DB --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index aa33f5ff1b..ba8b58fcea 100644 --- a/composer.lock +++ b/composer.lock @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5" + "reference": "44af82dcb44cdaa4b2d7f30528903f186a972ee0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e4a03ba543abc4e436ec1b450750a14bd36011d5", - "reference": "e4a03ba543abc4e436ec1b450750a14bd36011d5", + "url": "https://api.github.com/repos/utopia-php/database/zipball/44af82dcb44cdaa4b2d7f30528903f186a972ee0", + "reference": "44af82dcb44cdaa4b2d7f30528903f186a972ee0", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/2.0.0" + "source": "https://github.com/utopia-php/database/tree/2.0.1" }, - "time": "2025-09-04T12:36:53+00:00" + "time": "2025-09-11T08:33:25+00:00" }, { "name": "utopia-php/detector", From dd64afa97035a0608ee89be94e876a1e751d4f06 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 22:16:44 +1200 Subject: [PATCH 086/145] Add missing references --- docs/references/databases/create-operations.md | 0 docs/references/databases/create-transaction.md | 0 docs/references/databases/delete-transaction.md | 0 docs/references/databases/get-transaction.md | 0 docs/references/databases/list-transactions.md | 0 docs/references/databases/update-transaction.md | 0 docs/references/tablesdb/create-operations.md | 1 + docs/references/tablesdb/create-transaction.md | 1 + docs/references/tablesdb/delete-transaction.md | 1 + docs/references/tablesdb/get-transaction.md | 1 + docs/references/tablesdb/list-transactions.md | 1 + docs/references/tablesdb/update-transaction.md | 1 + 12 files changed, 6 insertions(+) create mode 100644 docs/references/databases/create-operations.md create mode 100644 docs/references/databases/create-transaction.md create mode 100644 docs/references/databases/delete-transaction.md create mode 100644 docs/references/databases/get-transaction.md create mode 100644 docs/references/databases/list-transactions.md create mode 100644 docs/references/databases/update-transaction.md create mode 100644 docs/references/tablesdb/create-operations.md create mode 100644 docs/references/tablesdb/create-transaction.md create mode 100644 docs/references/tablesdb/delete-transaction.md create mode 100644 docs/references/tablesdb/get-transaction.md create mode 100644 docs/references/tablesdb/list-transactions.md create mode 100644 docs/references/tablesdb/update-transaction.md diff --git a/docs/references/databases/create-operations.md b/docs/references/databases/create-operations.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/databases/create-transaction.md b/docs/references/databases/create-transaction.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/databases/delete-transaction.md b/docs/references/databases/delete-transaction.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/databases/get-transaction.md b/docs/references/databases/get-transaction.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/databases/list-transactions.md b/docs/references/databases/list-transactions.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/databases/update-transaction.md b/docs/references/databases/update-transaction.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/references/tablesdb/create-operations.md b/docs/references/tablesdb/create-operations.md new file mode 100644 index 0000000000..a737b95a55 --- /dev/null +++ b/docs/references/tablesdb/create-operations.md @@ -0,0 +1 @@ +Create multiple operations in a single transaction. \ No newline at end of file diff --git a/docs/references/tablesdb/create-transaction.md b/docs/references/tablesdb/create-transaction.md new file mode 100644 index 0000000000..fdf369a789 --- /dev/null +++ b/docs/references/tablesdb/create-transaction.md @@ -0,0 +1 @@ +Create a new transaction. \ No newline at end of file diff --git a/docs/references/tablesdb/delete-transaction.md b/docs/references/tablesdb/delete-transaction.md new file mode 100644 index 0000000000..f1395c228f --- /dev/null +++ b/docs/references/tablesdb/delete-transaction.md @@ -0,0 +1 @@ +Delete a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/tablesdb/get-transaction.md b/docs/references/tablesdb/get-transaction.md new file mode 100644 index 0000000000..41900f7468 --- /dev/null +++ b/docs/references/tablesdb/get-transaction.md @@ -0,0 +1 @@ +Get a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/tablesdb/list-transactions.md b/docs/references/tablesdb/list-transactions.md new file mode 100644 index 0000000000..9a63d9f04a --- /dev/null +++ b/docs/references/tablesdb/list-transactions.md @@ -0,0 +1 @@ +List transactions across all databases. \ No newline at end of file diff --git a/docs/references/tablesdb/update-transaction.md b/docs/references/tablesdb/update-transaction.md new file mode 100644 index 0000000000..d9d5f45439 --- /dev/null +++ b/docs/references/tablesdb/update-transaction.md @@ -0,0 +1 @@ +Update a transaction, to either commit or roll back its operations. \ No newline at end of file From c7dba3690a125f598e8c2d33080320e910e8facd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 22:17:00 +1200 Subject: [PATCH 087/145] Update auth types --- docs/references/databases/create-operations.md | 1 + docs/references/databases/create-transaction.md | 1 + docs/references/databases/delete-transaction.md | 1 + docs/references/databases/get-index.md | 2 +- docs/references/databases/get-transaction.md | 1 + docs/references/databases/list-transactions.md | 1 + docs/references/databases/update-transaction.md | 1 + .../Modules/Databases/Http/Databases/Transactions/Create.php | 2 +- .../Modules/Databases/Http/Databases/Transactions/Delete.php | 2 +- .../Modules/Databases/Http/Databases/Transactions/Get.php | 2 +- .../Modules/Databases/Http/Databases/Transactions/Update.php | 2 +- .../Modules/Databases/Http/Databases/Transactions/XList.php | 2 +- 12 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/references/databases/create-operations.md b/docs/references/databases/create-operations.md index e69de29bb2..a737b95a55 100644 --- a/docs/references/databases/create-operations.md +++ b/docs/references/databases/create-operations.md @@ -0,0 +1 @@ +Create multiple operations in a single transaction. \ No newline at end of file diff --git a/docs/references/databases/create-transaction.md b/docs/references/databases/create-transaction.md index e69de29bb2..fdf369a789 100644 --- a/docs/references/databases/create-transaction.md +++ b/docs/references/databases/create-transaction.md @@ -0,0 +1 @@ +Create a new transaction. \ No newline at end of file diff --git a/docs/references/databases/delete-transaction.md b/docs/references/databases/delete-transaction.md index e69de29bb2..f1395c228f 100644 --- a/docs/references/databases/delete-transaction.md +++ b/docs/references/databases/delete-transaction.md @@ -0,0 +1 @@ +Delete a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/databases/get-index.md b/docs/references/databases/get-index.md index cdea5b4f27..cdc27fa967 100644 --- a/docs/references/databases/get-index.md +++ b/docs/references/databases/get-index.md @@ -1 +1 @@ -Get index by ID. \ No newline at end of file +Get an index by its unique ID. \ No newline at end of file diff --git a/docs/references/databases/get-transaction.md b/docs/references/databases/get-transaction.md index e69de29bb2..41900f7468 100644 --- a/docs/references/databases/get-transaction.md +++ b/docs/references/databases/get-transaction.md @@ -0,0 +1 @@ +Get a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/databases/list-transactions.md b/docs/references/databases/list-transactions.md index e69de29bb2..9a63d9f04a 100644 --- a/docs/references/databases/list-transactions.md +++ b/docs/references/databases/list-transactions.md @@ -0,0 +1 @@ +List transactions across all databases. \ No newline at end of file diff --git a/docs/references/databases/update-transaction.md b/docs/references/databases/update-transaction.md index e69de29bb2..d9d5f45439 100644 --- a/docs/references/databases/update-transaction.md +++ b/docs/references/databases/update-transaction.md @@ -0,0 +1 @@ +Update a transaction, to either commit or roll back its operations. \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index 77589e848a..4a7e95d5ca 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -40,7 +40,7 @@ class Create extends Action group: 'transactions', name: 'createTransaction', description: '/docs/references/databases/create-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php index 81315498f6..43c0c5c4a8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php @@ -39,7 +39,7 @@ class Delete extends Action group: 'transactions', name: 'deleteTransaction', description: '/docs/references/databases/delete-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_NOCONTENT, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php index 6afe28d626..43406a6751 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php @@ -38,7 +38,7 @@ class Get extends Action group: 'transactions', name: 'getTransaction', description: '/docs/references/databases/get-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index dde39efd90..ed70afee35 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -48,7 +48,7 @@ class Update extends Action group: 'transactions', name: 'updateTransaction', description: '/docs/references/databases/update-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php index b13ca8c52b..05ef5ae9e8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php @@ -41,7 +41,7 @@ class XList extends Action group: 'transactions', name: 'listTransactions', description: '/docs/references/databases/list-transactions.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, From 6652e27dd59bec080f8382ad462bd1fafb410ea9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 22:21:53 +1200 Subject: [PATCH 088/145] Update TablesDB auth --- .../Modules/Databases/Http/Databases/Transactions/Action.php | 1 - .../Modules/Databases/Http/TablesDB/Transactions/Create.php | 2 +- .../Modules/Databases/Http/TablesDB/Transactions/Delete.php | 2 +- .../Modules/Databases/Http/TablesDB/Transactions/Get.php | 2 +- .../Databases/Http/TablesDB/Transactions/Operations/Create.php | 2 +- .../Modules/Databases/Http/TablesDB/Transactions/Update.php | 2 +- .../Modules/Databases/Http/TablesDB/Transactions/XList.php | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php index 512deb8fc8..8915ae6141 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; -use Appwrite\Extend\Exception; use Utopia\Platform\Action as UtopiaAction; abstract class Action extends UtopiaAction diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php index 77b87c77dc..6a28d31621 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php @@ -37,7 +37,7 @@ class Create extends TransactionsCreate group: 'transactions', name: 'createTransaction', description: '/docs/references/tablesdb/create-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php index 9440b586c5..cad11e795a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php @@ -37,7 +37,7 @@ class Delete extends TransactionsDelete group: 'transactions', name: 'deleteTransaction', description: '/docs/references/tablesdb/delete-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_NOCONTENT, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php index a5e9c374a6..39f6754a1e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php @@ -37,7 +37,7 @@ class Get extends TransactionsGet group: 'transactions', name: 'getTransaction', description: '/docs/references/tablesdb/get-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php index eac524682c..6b2ee2ce4c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -39,7 +39,7 @@ class Create extends OperationsCreate group: 'transactions', name: 'createOperations', description: '/docs/references/tablesdb/create-operations.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index d5b0e737a0..b754ec97a1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -38,7 +38,7 @@ class Update extends TransactionsUpdate group: 'transactions', name: 'updateTransaction', description: '/docs/references/tablesdb/update-transaction.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php index e04d28f200..67a58d7e5f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php @@ -37,7 +37,7 @@ class XList extends TransactionsList group: 'transactions', name: 'listTransactions', description: '/docs/references/tablesdb/list-transactions.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_OK, From b74e5ce5cd707817b1ccd201ca4d548a16f02af6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 22:22:46 +1200 Subject: [PATCH 089/145] Block bulk ops through txn create multi ops --- .../Databases/Transactions/Operations/Create.php | 15 ++++++++++++++- .../Utopia/Database/Validator/Operation.php | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 58505591ff..a33af05e3d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations; +use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action; use Appwrite\SDK\AuthType; @@ -13,6 +14,7 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\ArrayList; @@ -43,7 +45,7 @@ class Create extends Action group: 'transactions', name: 'createOperations', description: '/docs/references/databases/create-operations.md', - auth: [AuthType::KEY], + auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, @@ -77,8 +79,19 @@ class Create extends Action ); } + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $databases = $collections = $staged = []; foreach ($operations as $operation) { + if (!$isAPIKey && !$isPrivilegedUser && \in_array($operation['action'], [ + 'bulkCreate', + 'bulkUpdate', + 'bulkDelete' + ])) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + $database = $databases[$operation['databaseId']] ??= $dbForProject->getDocument('databases', $operation['databaseId']); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index ec2d4f0d49..f3fe2375b0 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -64,6 +64,16 @@ class Operation extends Validator return $this->description; } + public function getCollectionIdName(): string + { + return $this->collectionIdName; + } + + public function getDocumentIdName(): string + { + return $this->documentIdName; + } + public function isArray(): bool { return true; From 67b05a3f1ace8c333cf066061b54658631c6cb6e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 23:08:23 +1200 Subject: [PATCH 090/145] Fix test workflow --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b1a243da1..cebdc02163 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -248,7 +248,6 @@ jobs: Console, Databases/Legacy, Databases/TablesDB, - Databases/Transactions, Functions, FunctionsSchedule, GraphQL, From fd6ddf6926bab320fc80e5f2edc8c426af290ec6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Sep 2025 23:31:53 +1200 Subject: [PATCH 091/145] Fix operation spec gen --- app/config/specs/open-api3-1.8.x-client.json | 1081 ++++++++++++- app/config/specs/open-api3-1.8.x-console.json | 1405 +++++++++++++++-- app/config/specs/open-api3-1.8.x-server.json | 1366 ++++++++++++++-- app/config/specs/open-api3-latest-client.json | 1081 ++++++++++++- .../specs/open-api3-latest-console.json | 1405 +++++++++++++++-- app/config/specs/open-api3-latest-server.json | 1366 ++++++++++++++-- app/config/specs/swagger2-1.8.x-client.json | 1064 ++++++++++++- app/config/specs/swagger2-1.8.x-console.json | 1374 ++++++++++++++-- app/config/specs/swagger2-1.8.x-server.json | 1340 ++++++++++++++-- app/config/specs/swagger2-latest-client.json | 1064 ++++++++++++- app/config/specs/swagger2-latest-console.json | 1374 ++++++++++++++-- app/config/specs/swagger2-latest-server.json | 1340 ++++++++++++++-- .../SDK/Specification/Format/OpenAPI3.php | 95 +- .../SDK/Specification/Format/Swagger2.php | 73 +- .../Utopia/Database/Validator/Operation.php | 4 +- 15 files changed, 14213 insertions(+), 1219 deletions(-) diff --git a/app/config/specs/open-api3-1.8.x-client.json b/app/config/specs/open-api3-1.8.x-client.json index d226fcc4e1..e94ac895b6 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -4806,6 +4806,424 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents": { "get": { "summary": "List documents", @@ -4893,6 +5311,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -4951,7 +5379,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5037,6 +5466,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5142,6 +5576,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -5200,7 +5644,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5283,6 +5728,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -5396,6 +5846,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5480,7 +5935,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/{attribute}\/decrement": { @@ -5594,6 +6065,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5713,6 +6189,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5745,7 +6226,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -5820,7 +6301,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -5896,9 +6377,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -5936,7 +6417,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -7467,6 +7948,424 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows": { "get": { "summary": "List rows", @@ -7491,7 +8390,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -7553,6 +8452,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -7579,7 +8488,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -7610,7 +8519,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7692,6 +8602,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -7724,7 +8639,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -7796,6 +8711,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -7822,7 +8747,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -7853,7 +8778,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7931,6 +8857,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -7961,7 +8892,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -8040,6 +8971,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -8063,7 +8999,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -8123,7 +9059,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/{column}\/decrement": { @@ -8150,7 +9102,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -8236,6 +9188,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -8268,7 +9225,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -8354,6 +9311,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9935,6 +10897,34 @@ "localeCodes": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "row": { "description": "Row", "type": "object", @@ -11839,6 +12829,59 @@ "recoveryCode": true } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index 02d97fffc7..1ff651107b 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 508, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -5205,6 +5205,424 @@ } } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/usage": { "get": { "summary": "Get databases usage stats", @@ -9460,6 +9878,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -9518,7 +9946,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9549,7 +9978,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9634,6 +10064,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9693,7 +10128,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9759,6 +10195,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9860,6 +10301,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9953,6 +10399,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10058,6 +10509,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -10116,7 +10577,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -10199,6 +10661,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -10312,6 +10779,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10396,7 +10868,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/logs": { @@ -10607,6 +11095,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10726,6 +11219,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10960,7 +11458,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -11540,7 +12038,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -11613,7 +12111,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -11846,7 +12344,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -11895,7 +12393,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -11945,7 +12443,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 466, + "weight": 478, "cookies": false, "type": "", "demo": "functions\/list-templates.md", @@ -12045,7 +12543,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 465, + "weight": 477, "cookies": false, "type": "", "demo": "functions\/get-template.md", @@ -12105,7 +12603,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 459, + "weight": 471, "cookies": false, "type": "", "demo": "functions\/list-usage.md", @@ -12177,7 +12675,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -12236,7 +12734,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -12466,7 +12964,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -12527,7 +13025,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -12607,7 +13105,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -12690,7 +13188,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -12786,7 +13284,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -12871,7 +13369,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -12974,7 +13472,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -13071,7 +13569,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -13133,7 +13631,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -13197,7 +13695,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -13287,7 +13785,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -13358,7 +13856,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -13433,7 +13931,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -13509,9 +14007,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -13549,7 +14047,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -13614,7 +14112,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -13685,7 +14183,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 458, + "weight": 470, "cookies": false, "type": "", "demo": "functions\/get-usage.md", @@ -13767,7 +14265,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -13826,7 +14324,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -13917,7 +14415,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -13986,7 +14484,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -14077,7 +14575,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -16411,7 +16909,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -16592,7 +17090,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -21190,7 +21688,7 @@ "resourceId": { "type": "string", "description": "Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "internalFile": { "type": "boolean", @@ -22433,7 +22931,7 @@ "x-appwrite": { "method": "list", "group": "projects", - "weight": 436, + "weight": 448, "cookies": false, "type": "", "demo": "projects\/list.md", @@ -24068,7 +24566,7 @@ "x-appwrite": { "method": "listDevKeys", "group": "devKeys", - "weight": 434, + "weight": 446, "cookies": false, "type": "", "demo": "projects\/list-dev-keys.md", @@ -24106,7 +24604,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: accessedAt, expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -24136,7 +24637,7 @@ "x-appwrite": { "method": "createDevKey", "group": "devKeys", - "weight": 431, + "weight": 443, "cookies": false, "type": "", "demo": "projects\/create-dev-key.md", @@ -24221,7 +24722,7 @@ "x-appwrite": { "method": "getDevKey", "group": "devKeys", - "weight": 433, + "weight": 445, "cookies": false, "type": "", "demo": "projects\/get-dev-key.md", @@ -24289,7 +24790,7 @@ "x-appwrite": { "method": "updateDevKey", "group": "devKeys", - "weight": 432, + "weight": 444, "cookies": false, "type": "", "demo": "projects\/update-dev-key.md", @@ -24375,7 +24876,7 @@ "x-appwrite": { "method": "deleteDevKey", "group": "devKeys", - "weight": 435, + "weight": 447, "cookies": false, "type": "", "demo": "projects\/delete-dev-key.md", @@ -28209,7 +28710,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 514, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28784,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 509, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28851,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 511, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28929,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 512, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +29042,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 510, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +29120,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 513, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +29171,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 515, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +29231,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 516, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -28790,7 +29291,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -28819,7 +29320,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: name, enabled, framework, deploymentId, buildCommand, installCommand, outputDirectory, installationId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -28860,7 +29364,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -29109,7 +29613,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -29158,7 +29662,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29208,7 +29712,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 503, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29308,7 +29812,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 504, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29368,7 +29872,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 505, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -29440,7 +29944,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -29499,7 +30003,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -29744,7 +30248,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -29805,7 +30309,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -29885,7 +30389,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -29968,7 +30472,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -30069,7 +30573,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -30149,7 +30653,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -30252,7 +30756,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -30350,7 +30854,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -30412,7 +30916,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -30476,7 +30980,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -30566,7 +31070,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -30637,7 +31141,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30676,7 +31180,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: trigger, status, responseStatusCode, duration, requestMethod, requestPath, deploymentId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -30708,7 +31215,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30770,7 +31277,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30841,7 +31348,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 506, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -30923,7 +31430,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -30982,7 +31489,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31073,7 +31580,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31142,7 +31649,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31233,7 +31740,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -32705,7 +33212,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -32778,7 +33285,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -32833,6 +33340,424 @@ } } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/usage": { "get": { "summary": "Get TablesDB usage stats", @@ -32857,7 +33782,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 378, + "weight": 384, "cookies": false, "type": "", "demo": "tablesdb\/list-usage.md", @@ -32954,7 +33879,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -33013,7 +33938,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -33089,7 +34014,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -33150,7 +34075,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -33236,7 +34161,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -33343,7 +34268,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -33415,7 +34340,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -33517,7 +34442,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -33591,7 +34516,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -33678,7 +34603,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -33787,7 +34712,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -33901,7 +34826,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -34010,7 +34935,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -34124,7 +35049,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -34233,7 +35158,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -34347,7 +35272,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -34465,7 +35390,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -34588,7 +35513,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -34707,7 +35632,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -34831,7 +35756,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -34950,7 +35875,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -35074,7 +35999,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -35183,7 +36108,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -35297,7 +36222,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -35409,7 +36334,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -35529,7 +36454,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -35641,7 +36566,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -35761,7 +36686,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -35873,7 +36798,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -35993,7 +36918,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -36127,7 +37052,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -36247,7 +37172,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -36366,7 +37291,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -36475,7 +37400,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -36620,7 +37545,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -36694,7 +37619,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -36777,7 +37702,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -36888,7 +37813,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -36973,7 +37898,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -37105,7 +38030,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -37179,7 +38104,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -37262,7 +38187,7 @@ "x-appwrite": { "method": "listTableLogs", "group": "tables", - "weight": 384, + "weight": 390, "cookies": false, "type": "", "demo": "tablesdb\/list-table-logs.md", @@ -37348,7 +38273,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -37410,6 +38335,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -37436,7 +38371,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -37467,7 +38402,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -37494,7 +38430,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37575,6 +38512,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37605,7 +38547,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -37633,7 +38575,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37695,6 +38638,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -37728,7 +38676,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -37795,6 +38743,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37825,7 +38778,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -37887,6 +38840,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37919,7 +38877,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -37991,6 +38949,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -38017,7 +38985,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -38048,7 +39016,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -38126,6 +39095,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38156,7 +39130,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -38235,6 +39209,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38258,7 +39237,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -38318,7 +39297,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/logs": { @@ -38345,7 +39340,7 @@ "x-appwrite": { "method": "listRowLogs", "group": "logs", - "weight": 428, + "weight": 434, "cookies": false, "type": "", "demo": "tablesdb\/list-row-logs.md", @@ -38441,7 +39436,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -38527,6 +39522,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38559,7 +39559,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -38645,6 +39645,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38677,7 +39682,7 @@ "x-appwrite": { "method": "getTableUsage", "group": null, - "weight": 385, + "weight": 391, "cookies": false, "type": "", "demo": "tablesdb\/get-table-usage.md", @@ -38772,7 +39777,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 377, + "weight": 383, "cookies": false, "type": "", "demo": "tablesdb\/get-usage.md", @@ -39984,7 +40989,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40034,7 +41039,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -40064,7 +41072,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40153,7 +41161,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40213,7 +41221,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40283,7 +41291,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -46107,6 +47115,34 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "migrationList": { "description": "Migrations List", "type": "object", @@ -56505,6 +57541,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index 09d53dbdf0..987ae7fece 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -4743,6 +4743,436 @@ } } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/{databaseId}": { "get": { "summary": "Get database", @@ -8938,6 +9368,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -8997,7 +9437,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9029,7 +9470,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9116,6 +9558,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9176,7 +9623,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9243,6 +9691,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9345,6 +9798,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9439,6 +9897,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9546,6 +10009,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -9605,7 +10078,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9690,6 +10164,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9805,6 +10284,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9891,7 +10375,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/{attribute}\/decrement": { @@ -10007,6 +10507,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10128,6 +10633,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10364,7 +10874,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -10542,7 +11052,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -10616,7 +11126,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -10850,7 +11360,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -10900,7 +11410,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -10951,7 +11461,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -11011,7 +11521,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -11242,7 +11752,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -11304,7 +11814,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -11385,7 +11895,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -11469,7 +11979,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -11566,7 +12076,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -11652,7 +12162,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -11756,7 +12266,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -11854,7 +12364,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -11917,7 +12427,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -11982,7 +12492,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -12073,7 +12583,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -12145,7 +12655,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -12222,7 +12732,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -12300,9 +12810,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -12340,7 +12850,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -12407,7 +12917,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -12479,7 +12989,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -12539,7 +13049,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -12631,7 +13141,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -12701,7 +13211,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -12793,7 +13303,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -15174,7 +15684,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -15356,7 +15866,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -19717,7 +20227,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -19747,7 +20257,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: name, enabled, framework, deploymentId, buildCommand, installCommand, outputDirectory, installationId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -19788,7 +20301,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -20038,7 +20551,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -20088,7 +20601,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -20139,7 +20652,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -20199,7 +20712,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -20445,7 +20958,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -20507,7 +21020,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -20588,7 +21101,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -20672,7 +21185,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -20774,7 +21287,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -20855,7 +21368,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -20959,7 +21472,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -21058,7 +21571,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -21121,7 +21634,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -21186,7 +21699,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -21277,7 +21790,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -21349,7 +21862,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21389,7 +21902,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: trigger, status, responseStatusCode, duration, requestMethod, requestPath, deploymentId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -21421,7 +21937,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21484,7 +22000,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21556,7 +22072,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21616,7 +22132,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21708,7 +22224,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21778,7 +22294,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21870,7 +22386,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -23210,7 +23726,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -23284,7 +23800,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -23340,6 +23856,436 @@ } } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/{databaseId}": { "get": { "summary": "Get database", @@ -23364,7 +24310,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -23424,7 +24370,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -23501,7 +24447,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -23563,7 +24509,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -23650,7 +24596,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -23758,7 +24704,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -23831,7 +24777,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -23934,7 +24880,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -24009,7 +24955,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -24097,7 +25043,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -24207,7 +25153,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -24322,7 +25268,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -24432,7 +25378,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -24547,7 +25493,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -24657,7 +25603,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -24772,7 +25718,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -24891,7 +25837,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -25015,7 +25961,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -25135,7 +26081,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -25260,7 +26206,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -25380,7 +26326,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -25505,7 +26451,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -25615,7 +26561,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -25730,7 +26676,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -25843,7 +26789,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -25964,7 +26910,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -26077,7 +27023,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -26198,7 +27144,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -26311,7 +27257,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -26432,7 +27378,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -26567,7 +27513,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -26688,7 +27634,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -26808,7 +27754,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -26918,7 +27864,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -27064,7 +28010,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -27139,7 +28085,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -27223,7 +28169,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -27335,7 +28281,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -27421,7 +28367,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -27554,7 +28500,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -27629,7 +28575,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -27713,7 +28659,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -27777,6 +28723,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -27803,7 +28759,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -27835,7 +28791,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -27863,7 +28820,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -27946,6 +28904,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -27976,7 +28939,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -28005,7 +28968,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28068,6 +29032,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -28101,7 +29070,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -28169,6 +29138,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28199,7 +29173,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -28262,6 +29236,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28294,7 +29273,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -28368,6 +29347,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -28394,7 +29383,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -28426,7 +29415,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -28506,6 +29496,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28536,7 +29531,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -28617,6 +29612,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28640,7 +29640,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -28702,7 +29702,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/{column}\/decrement": { @@ -28729,7 +29745,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -28817,6 +29833,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28849,7 +29870,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -28937,6 +29958,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -30024,7 +31050,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30075,7 +31101,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -30105,7 +31134,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30195,7 +31224,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30256,7 +31285,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30327,7 +31356,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -35033,6 +36062,34 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "specificationList": { "description": "Specifications List", "type": "object", @@ -41438,6 +42495,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index d226fcc4e1..a24d36fe76 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -4806,6 +4806,424 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents": { "get": { "summary": "List documents", @@ -4893,6 +5311,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -4951,7 +5379,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5037,6 +5466,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5142,6 +5576,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -5200,7 +5644,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5283,6 +5728,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -5396,6 +5846,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5480,7 +5935,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/{attribute}\/decrement": { @@ -5594,6 +6065,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5713,6 +6189,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -5745,7 +6226,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -5820,7 +6301,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -5896,9 +6377,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -5936,7 +6417,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -7467,6 +7948,424 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows": { "get": { "summary": "List rows", @@ -7491,7 +8390,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -7553,6 +8452,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -7579,7 +8488,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -7610,7 +8519,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7692,6 +8602,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -7724,7 +8639,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -7796,6 +8711,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -7822,7 +8747,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -7853,7 +8778,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7931,6 +8857,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -7961,7 +8892,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -8040,6 +8971,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -8063,7 +8999,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -8123,7 +9059,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/{column}\/decrement": { @@ -8150,7 +9102,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -8236,6 +9188,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -8268,7 +9225,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -8354,6 +9311,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9935,6 +10897,34 @@ "localeCodes": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "row": { "description": "Row", "type": "object", @@ -11839,6 +12829,59 @@ "recoveryCode": true } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 02d97fffc7..3316be2f8b 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 508, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -5205,6 +5205,424 @@ } } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/usage": { "get": { "summary": "Get databases usage stats", @@ -9460,6 +9878,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -9518,7 +9946,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9549,7 +9978,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9634,6 +10064,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9693,7 +10128,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9759,6 +10195,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9860,6 +10301,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9953,6 +10399,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10058,6 +10509,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -10116,7 +10577,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -10199,6 +10661,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -10312,6 +10779,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10396,7 +10868,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/logs": { @@ -10607,6 +11095,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10726,6 +11219,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10960,7 +11458,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -11540,7 +12038,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -11613,7 +12111,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -11846,7 +12344,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -11895,7 +12393,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -11945,7 +12443,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 466, + "weight": 478, "cookies": false, "type": "", "demo": "functions\/list-templates.md", @@ -12045,7 +12543,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 465, + "weight": 477, "cookies": false, "type": "", "demo": "functions\/get-template.md", @@ -12105,7 +12603,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 459, + "weight": 471, "cookies": false, "type": "", "demo": "functions\/list-usage.md", @@ -12177,7 +12675,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -12236,7 +12734,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -12466,7 +12964,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -12527,7 +13025,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -12607,7 +13105,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -12690,7 +13188,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -12786,7 +13284,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -12871,7 +13369,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -12974,7 +13472,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -13071,7 +13569,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -13133,7 +13631,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -13197,7 +13695,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -13287,7 +13785,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -13358,7 +13856,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -13433,7 +13931,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -13509,9 +14007,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -13549,7 +14047,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -13614,7 +14112,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -13685,7 +14183,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 458, + "weight": 470, "cookies": false, "type": "", "demo": "functions\/get-usage.md", @@ -13767,7 +14265,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -13826,7 +14324,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -13917,7 +14415,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -13986,7 +14484,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -14077,7 +14575,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -16411,7 +16909,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -16592,7 +17090,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -21190,7 +21688,7 @@ "resourceId": { "type": "string", "description": "Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "internalFile": { "type": "boolean", @@ -22433,7 +22931,7 @@ "x-appwrite": { "method": "list", "group": "projects", - "weight": 436, + "weight": 448, "cookies": false, "type": "", "demo": "projects\/list.md", @@ -24068,7 +24566,7 @@ "x-appwrite": { "method": "listDevKeys", "group": "devKeys", - "weight": 434, + "weight": 446, "cookies": false, "type": "", "demo": "projects\/list-dev-keys.md", @@ -24106,7 +24604,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: accessedAt, expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -24136,7 +24637,7 @@ "x-appwrite": { "method": "createDevKey", "group": "devKeys", - "weight": 431, + "weight": 443, "cookies": false, "type": "", "demo": "projects\/create-dev-key.md", @@ -24221,7 +24722,7 @@ "x-appwrite": { "method": "getDevKey", "group": "devKeys", - "weight": 433, + "weight": 445, "cookies": false, "type": "", "demo": "projects\/get-dev-key.md", @@ -24289,7 +24790,7 @@ "x-appwrite": { "method": "updateDevKey", "group": "devKeys", - "weight": 432, + "weight": 444, "cookies": false, "type": "", "demo": "projects\/update-dev-key.md", @@ -24375,7 +24876,7 @@ "x-appwrite": { "method": "deleteDevKey", "group": "devKeys", - "weight": 435, + "weight": 447, "cookies": false, "type": "", "demo": "projects\/delete-dev-key.md", @@ -28209,7 +28710,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 514, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28784,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 509, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28851,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 511, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28929,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 512, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +29042,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 510, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +29120,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 513, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +29171,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 515, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +29231,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 516, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -28790,7 +29291,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -28819,7 +29320,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: name, enabled, framework, deploymentId, buildCommand, installCommand, outputDirectory, installationId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -28860,7 +29364,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -29109,7 +29613,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -29158,7 +29662,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29208,7 +29712,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 503, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29308,7 +29812,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 504, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29368,7 +29872,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 505, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -29440,7 +29944,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -29499,7 +30003,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -29744,7 +30248,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -29805,7 +30309,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -29885,7 +30389,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -29968,7 +30472,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -30069,7 +30573,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -30149,7 +30653,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -30252,7 +30756,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -30350,7 +30854,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -30412,7 +30916,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -30476,7 +30980,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -30566,7 +31070,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -30637,7 +31141,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30676,7 +31180,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: trigger, status, responseStatusCode, duration, requestMethod, requestPath, deploymentId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -30708,7 +31215,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30770,7 +31277,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30841,7 +31348,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 506, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -30923,7 +31430,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -30982,7 +31489,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31073,7 +31580,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31142,7 +31649,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31233,7 +31740,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -32705,7 +33212,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -32778,7 +33285,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -32833,6 +33340,424 @@ } } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/usage": { "get": { "summary": "Get TablesDB usage stats", @@ -32857,7 +33782,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 378, + "weight": 384, "cookies": false, "type": "", "demo": "tablesdb\/list-usage.md", @@ -32954,7 +33879,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -33013,7 +33938,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -33089,7 +34014,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -33150,7 +34075,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -33236,7 +34161,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -33343,7 +34268,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -33415,7 +34340,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -33517,7 +34442,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -33591,7 +34516,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -33678,7 +34603,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -33787,7 +34712,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -33901,7 +34826,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -34010,7 +34935,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -34124,7 +35049,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -34233,7 +35158,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -34347,7 +35272,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -34465,7 +35390,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -34588,7 +35513,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -34707,7 +35632,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -34831,7 +35756,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -34950,7 +35875,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -35074,7 +35999,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -35183,7 +36108,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -35297,7 +36222,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -35409,7 +36334,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -35529,7 +36454,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -35641,7 +36566,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -35761,7 +36686,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -35873,7 +36798,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -35993,7 +36918,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -36127,7 +37052,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -36247,7 +37172,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -36366,7 +37291,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -36475,7 +37400,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -36620,7 +37545,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -36694,7 +37619,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -36777,7 +37702,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -36888,7 +37813,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -36973,7 +37898,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -37105,7 +38030,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -37179,7 +38104,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -37262,7 +38187,7 @@ "x-appwrite": { "method": "listTableLogs", "group": "tables", - "weight": 384, + "weight": 390, "cookies": false, "type": "", "demo": "tablesdb\/list-table-logs.md", @@ -37348,7 +38273,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -37410,6 +38335,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -37436,7 +38371,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -37467,7 +38402,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -37494,7 +38430,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37575,6 +38512,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37605,7 +38547,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -37633,7 +38575,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37695,6 +38638,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -37728,7 +38676,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -37795,6 +38743,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37825,7 +38778,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -37887,6 +38840,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -37919,7 +38877,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -37991,6 +38949,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -38017,7 +38985,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -38048,7 +39016,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -38126,6 +39095,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38156,7 +39130,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -38235,6 +39209,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38258,7 +39237,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -38318,7 +39297,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/logs": { @@ -38345,7 +39340,7 @@ "x-appwrite": { "method": "listRowLogs", "group": "logs", - "weight": 428, + "weight": 434, "cookies": false, "type": "", "demo": "tablesdb\/list-row-logs.md", @@ -38441,7 +39436,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -38527,6 +39522,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38559,7 +39559,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -38645,6 +39645,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -38677,7 +39682,7 @@ "x-appwrite": { "method": "getTableUsage", "group": null, - "weight": 385, + "weight": 391, "cookies": false, "type": "", "demo": "tablesdb\/get-table-usage.md", @@ -38772,7 +39777,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 377, + "weight": 383, "cookies": false, "type": "", "demo": "tablesdb\/get-usage.md", @@ -39984,7 +40989,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40034,7 +41039,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -40064,7 +41072,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40153,7 +41161,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40213,7 +41221,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40283,7 +41291,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -46107,6 +47115,34 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "migrationList": { "description": "Migrations List", "type": "object", @@ -56505,6 +57541,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 09d53dbdf0..a2d91def99 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -4743,6 +4743,436 @@ } } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/databases\/{databaseId}": { "get": { "summary": "Get database", @@ -8938,6 +9368,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -8997,7 +9437,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9029,7 +9470,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9116,6 +9558,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9176,7 +9623,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9243,6 +9691,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9345,6 +9798,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9439,6 +9897,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9546,6 +10009,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -9605,7 +10078,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9690,6 +10164,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -9805,6 +10284,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -9891,7 +10375,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents\/{documentId}\/{attribute}\/decrement": { @@ -10007,6 +10507,11 @@ "type": "number", "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10128,6 +10633,11 @@ "type": "number", "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -10364,7 +10874,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -10542,7 +11052,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -10616,7 +11126,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -10850,7 +11360,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -10900,7 +11410,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -10951,7 +11461,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -11011,7 +11521,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -11242,7 +11752,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -11304,7 +11814,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -11385,7 +11895,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -11469,7 +11979,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -11566,7 +12076,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -11652,7 +12162,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -11756,7 +12266,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -11854,7 +12364,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -11917,7 +12427,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -11982,7 +12492,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -12073,7 +12583,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -12145,7 +12655,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -12222,7 +12732,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -12300,9 +12810,9 @@ "x-enum-keys": [] }, "headers": { - "type": "string", + "type": "object", "description": "HTTP headers of execution. Defaults to empty.", - "x-example": null + "x-example": "{}" }, "scheduledAt": { "type": "string", @@ -12340,7 +12850,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -12407,7 +12917,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -12479,7 +12989,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -12539,7 +13049,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -12631,7 +13141,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -12701,7 +13211,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -12793,7 +13303,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -15174,7 +15684,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -15356,7 +15866,7 @@ "image": { "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -19717,7 +20227,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -19747,7 +20257,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: name, enabled, framework, deploymentId, buildCommand, installCommand, outputDirectory, installationId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -19788,7 +20301,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -20038,7 +20551,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -20088,7 +20601,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -20139,7 +20652,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -20199,7 +20712,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -20445,7 +20958,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -20507,7 +21020,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -20588,7 +21101,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -20672,7 +21185,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -20774,7 +21287,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -20855,7 +21368,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -20959,7 +21472,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -21058,7 +21571,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -21121,7 +21634,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -21186,7 +21699,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -21277,7 +21790,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -21349,7 +21862,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21389,7 +21902,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: trigger, status, responseStatusCode, duration, requestMethod, requestPath, deploymentId", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -21421,7 +21937,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21484,7 +22000,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21556,7 +22072,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21616,7 +22132,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21708,7 +22224,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21778,7 +22294,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21870,7 +22386,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -23210,7 +23726,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -23284,7 +23800,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -23340,6 +23856,436 @@ } } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transactionList" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "x-example": 60 + } + } + } + } + } + } + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "x-example": false + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/transaction" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client", + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, "\/tablesdb\/{databaseId}": { "get": { "summary": "Get database", @@ -23364,7 +24310,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -23424,7 +24370,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -23501,7 +24447,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -23563,7 +24509,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -23650,7 +24596,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -23758,7 +24704,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -23831,7 +24777,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -23934,7 +24880,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -24009,7 +24955,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -24097,7 +25043,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -24207,7 +25153,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -24322,7 +25268,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -24432,7 +25378,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -24547,7 +25493,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -24657,7 +25603,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -24772,7 +25718,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -24891,7 +25837,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -25015,7 +25961,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -25135,7 +26081,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -25260,7 +26206,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -25380,7 +26326,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -25505,7 +26451,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -25615,7 +26561,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -25730,7 +26676,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -25843,7 +26789,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -25964,7 +26910,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -26077,7 +27023,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -26198,7 +27144,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -26311,7 +27257,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -26432,7 +27378,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -26567,7 +27513,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -26688,7 +27634,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -26808,7 +27754,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -26918,7 +27864,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -27064,7 +28010,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -27139,7 +28085,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -27223,7 +28169,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -27335,7 +28281,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -27421,7 +28367,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -27554,7 +28500,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -27629,7 +28575,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -27713,7 +28659,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -27777,6 +28723,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -27803,7 +28759,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -27835,7 +28791,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -27863,7 +28820,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -27946,6 +28904,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -27976,7 +28939,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -28005,7 +28968,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28068,6 +29032,11 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } }, "required": [ @@ -28101,7 +29070,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -28169,6 +29138,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28199,7 +29173,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -28262,6 +29236,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28294,7 +29273,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -28368,6 +29347,16 @@ "default": [] }, "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" } ] }, @@ -28394,7 +29383,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -28426,7 +29415,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -28506,6 +29496,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28536,7 +29531,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -28617,6 +29612,11 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28640,7 +29640,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -28702,7 +29702,23 @@ }, "in": "path" } - ] + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" + } + } + } + } + } + } } }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows\/{rowId}\/{column}\/decrement": { @@ -28729,7 +29745,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -28817,6 +29833,11 @@ "type": "number", "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -28849,7 +29870,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -28937,6 +29958,11 @@ "type": "number", "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "x-example": "" } } } @@ -30024,7 +31050,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30075,7 +31101,10 @@ "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: expire", "required": false, "schema": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "default": [] }, "in": "query" @@ -30105,7 +31134,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30195,7 +31224,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30256,7 +31285,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30327,7 +31356,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -35033,6 +36062,34 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "$ref": "#\/components\/schemas\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "specificationList": { "description": "Specifications List", "type": "object", @@ -41438,6 +42495,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index 55911e9556..7297412924 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -4948,6 +4948,419 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents": { "get": { "summary": "List documents", @@ -5029,6 +5442,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -5088,7 +5509,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5173,6 +5595,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5269,6 +5697,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -5328,7 +5764,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5406,6 +5843,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -5514,6 +5957,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5593,6 +6042,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -5702,6 +6166,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5814,6 +6284,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5845,7 +6321,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -5918,7 +6394,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -6035,7 +6511,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -7549,6 +8025,419 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows": { "get": { "summary": "List rows", @@ -7573,7 +8462,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -7629,6 +8518,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -7657,7 +8554,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -7687,7 +8584,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7768,6 +8666,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -7799,7 +8703,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -7863,6 +8767,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -7891,7 +8803,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -7921,7 +8833,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7994,6 +8907,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8025,7 +8944,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -8098,6 +9017,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8124,7 +9049,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -8176,6 +9101,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -8206,7 +9146,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -8284,6 +9224,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8317,7 +9263,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -8395,6 +9341,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9931,6 +10883,35 @@ "localeCodes": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "row": { "description": "Row", "type": "object", @@ -11840,6 +12821,59 @@ "recoveryCode": true } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 6d5721c73b..5e8aad66e4 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 508, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -5367,6 +5367,419 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/usage": { "get": { "summary": "Get databases usage stats", @@ -9525,6 +9938,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9584,7 +10005,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9615,7 +10037,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9699,6 +10122,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9759,7 +10188,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9821,6 +10251,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9920,6 +10356,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10010,6 +10452,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10106,6 +10554,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -10165,7 +10621,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -10243,6 +10700,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -10351,6 +10814,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10430,6 +10899,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -10629,6 +11113,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10741,6 +11231,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10974,7 +11470,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -11524,7 +12020,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -11596,7 +12092,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -11847,7 +12343,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -11896,7 +12392,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -11946,7 +12442,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 466, + "weight": 478, "cookies": false, "type": "", "demo": "functions\/list-templates.md", @@ -12040,7 +12536,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 465, + "weight": 477, "cookies": false, "type": "", "demo": "functions\/get-template.md", @@ -12098,7 +12594,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 459, + "weight": 471, "cookies": false, "type": "", "demo": "functions\/list-usage.md", @@ -12168,7 +12664,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -12227,7 +12723,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -12474,7 +12970,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -12535,7 +13031,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -12612,7 +13108,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -12692,7 +13188,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -12784,7 +13280,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -12869,7 +13365,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -12975,7 +13471,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -13071,7 +13567,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -13133,7 +13629,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -13200,7 +13696,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -13285,7 +13781,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -13352,7 +13848,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -13425,7 +13921,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -13542,7 +14038,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -13606,7 +14102,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -13673,7 +14169,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 458, + "weight": 470, "cookies": false, "type": "", "demo": "functions\/get-usage.md", @@ -13751,7 +14247,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -13810,7 +14306,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -13900,7 +14396,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -13967,7 +14463,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -14059,7 +14555,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -16423,7 +16919,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": "", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -16620,7 +17116,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -21355,7 +21851,7 @@ "type": "string", "description": "Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "internalFile": { "type": "boolean", @@ -22590,7 +23086,7 @@ "x-appwrite": { "method": "list", "group": "projects", - "weight": 436, + "weight": 448, "cookies": false, "type": "", "demo": "projects\/list.md", @@ -24233,7 +24729,7 @@ "x-appwrite": { "method": "listDevKeys", "group": "devKeys", - "weight": 434, + "weight": 446, "cookies": false, "type": "", "demo": "projects\/list-dev-keys.md", @@ -24303,7 +24799,7 @@ "x-appwrite": { "method": "createDevKey", "group": "devKeys", - "weight": 431, + "weight": 443, "cookies": false, "type": "", "demo": "projects\/create-dev-key.md", @@ -24386,7 +24882,7 @@ "x-appwrite": { "method": "getDevKey", "group": "devKeys", - "weight": 433, + "weight": 445, "cookies": false, "type": "", "demo": "projects\/get-dev-key.md", @@ -24452,7 +24948,7 @@ "x-appwrite": { "method": "updateDevKey", "group": "devKeys", - "weight": 432, + "weight": 444, "cookies": false, "type": "", "demo": "projects\/update-dev-key.md", @@ -24538,7 +25034,7 @@ "x-appwrite": { "method": "deleteDevKey", "group": "devKeys", - "weight": 435, + "weight": 447, "cookies": false, "type": "", "demo": "projects\/delete-dev-key.md", @@ -28351,7 +28847,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 514, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28920,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 509, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28990,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 511, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +29073,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 512, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +29193,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 510, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +29274,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 513, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +29327,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 515, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +29387,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 516, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -28949,7 +29445,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -29021,7 +29517,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -29288,7 +29784,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -29337,7 +29833,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29387,7 +29883,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 503, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29481,7 +29977,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 504, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29539,7 +30035,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 505, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -29609,7 +30105,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -29668,7 +30164,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -29930,7 +30426,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -29991,7 +30487,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -30068,7 +30564,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -30148,7 +30644,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -30248,7 +30744,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -30327,7 +30823,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -30433,7 +30929,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -30530,7 +31026,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -30592,7 +31088,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -30659,7 +31155,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -30744,7 +31240,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -30811,7 +31307,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30882,7 +31378,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30946,7 +31442,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31013,7 +31509,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 506, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31091,7 +31587,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31150,7 +31646,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31240,7 +31736,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31307,7 +31803,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31399,7 +31895,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -32833,7 +33329,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -32905,7 +33401,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -32963,6 +33459,419 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/usage": { "get": { "summary": "Get TablesDB usage stats", @@ -32987,7 +33896,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 378, + "weight": 384, "cookies": false, "type": "", "demo": "tablesdb\/list-usage.md", @@ -33082,7 +33991,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -33141,7 +34050,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -33219,7 +34128,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -33278,7 +34187,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -33361,7 +34270,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -33469,7 +34378,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -33539,7 +34448,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -33643,7 +34552,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -33713,7 +34622,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -33797,7 +34706,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -33906,7 +34815,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -34017,7 +34926,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -34126,7 +35035,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -34237,7 +35146,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -34346,7 +35255,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -34457,7 +35366,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -34576,7 +35485,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -34697,7 +35606,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -34818,7 +35727,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -34941,7 +35850,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -35062,7 +35971,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -35185,7 +36094,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -35294,7 +36203,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -35405,7 +36314,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -35509,7 +36418,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -35619,7 +36528,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -35723,7 +36632,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -35833,7 +36742,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -35937,7 +36846,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -36047,7 +36956,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -36183,7 +37092,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -36305,7 +37214,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -36422,7 +37331,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -36531,7 +37440,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -36671,7 +37580,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -36743,7 +37652,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -36822,7 +37731,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -36927,7 +37836,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -37009,7 +37918,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -37140,7 +38049,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -37212,7 +38121,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -37289,7 +38198,7 @@ "x-appwrite": { "method": "listTableLogs", "group": "tables", - "weight": 384, + "weight": 390, "cookies": false, "type": "", "demo": "tablesdb\/list-table-logs.md", @@ -37370,7 +38279,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -37426,6 +38335,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -37454,7 +38371,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -37484,7 +38401,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -37511,7 +38429,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37591,6 +38510,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37622,7 +38547,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -37650,7 +38575,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37708,6 +38634,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -37742,7 +38674,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -37806,6 +38738,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37837,7 +38775,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -37895,6 +38833,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37926,7 +38870,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -37990,6 +38934,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -38018,7 +38970,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -38048,7 +39000,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -38121,6 +39074,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38152,7 +39111,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -38225,6 +39184,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38251,7 +39216,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -38303,6 +39268,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -38331,7 +39311,7 @@ "x-appwrite": { "method": "listRowLogs", "group": "logs", - "weight": 428, + "weight": 434, "cookies": false, "type": "", "demo": "tablesdb\/list-row-logs.md", @@ -38422,7 +39402,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -38500,6 +39480,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38533,7 +39519,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -38611,6 +39597,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38642,7 +39634,7 @@ "x-appwrite": { "method": "getTableUsage", "group": null, - "weight": 385, + "weight": 391, "cookies": false, "type": "", "demo": "tablesdb\/get-table-usage.md", @@ -38731,7 +39723,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 377, + "weight": 383, "cookies": false, "type": "", "demo": "tablesdb\/get-usage.md", @@ -39916,7 +40908,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -39996,7 +40988,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40080,7 +41072,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40140,7 +41132,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40211,7 +41203,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -46040,6 +47032,35 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "migrationList": { "description": "Migrations List", "type": "object", @@ -56554,6 +57575,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index 98077f1050..9dd44734bb 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -4893,6 +4893,431 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/{databaseId}": { "get": { "summary": "Get database", @@ -8993,6 +9418,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9053,7 +9486,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9085,7 +9519,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9171,6 +9606,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9232,7 +9673,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9295,6 +9737,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9395,6 +9843,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9486,6 +9940,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9584,6 +10044,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9644,7 +10112,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9724,6 +10193,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9834,6 +10309,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9915,6 +10396,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -10026,6 +10522,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10140,6 +10642,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10375,7 +10883,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -10541,7 +11049,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -10614,7 +11122,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -10866,7 +11374,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -10916,7 +11424,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -10967,7 +11475,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -11027,7 +11535,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -11275,7 +11783,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -11337,7 +11845,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -11415,7 +11923,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -11496,7 +12004,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -11589,7 +12097,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -11675,7 +12183,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -11782,7 +12290,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -11879,7 +12387,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -11942,7 +12450,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -12010,7 +12518,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -12096,7 +12604,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -12164,7 +12672,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -12239,7 +12747,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -12358,7 +12866,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -12424,7 +12932,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -12492,7 +13000,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -12552,7 +13060,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -12643,7 +13151,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -12711,7 +13219,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -12804,7 +13312,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -15215,7 +15723,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": "", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -15413,7 +15921,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -19909,7 +20417,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -19982,7 +20490,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -20250,7 +20758,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -20300,7 +20808,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -20351,7 +20859,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -20411,7 +20919,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -20674,7 +21182,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -20736,7 +21244,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -20814,7 +21322,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -20895,7 +21403,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -20996,7 +21504,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -21076,7 +21584,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -21183,7 +21691,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -21281,7 +21789,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -21344,7 +21852,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -21412,7 +21920,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -21498,7 +22006,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -21566,7 +22074,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21638,7 +22146,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21703,7 +22211,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21771,7 +22279,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21831,7 +22339,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21922,7 +22430,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21990,7 +22498,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22083,7 +22591,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -23391,7 +23899,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -23464,7 +23972,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -23523,6 +24031,431 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/{databaseId}": { "get": { "summary": "Get database", @@ -23547,7 +24480,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -23607,7 +24540,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -23686,7 +24619,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -23746,7 +24679,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -23830,7 +24763,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -23939,7 +24872,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -24010,7 +24943,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -24115,7 +25048,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -24186,7 +25119,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -24271,7 +25204,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -24381,7 +25314,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -24493,7 +25426,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -24603,7 +25536,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -24715,7 +25648,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -24825,7 +25758,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -24937,7 +25870,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -25057,7 +25990,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -25179,7 +26112,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -25301,7 +26234,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -25425,7 +26358,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -25547,7 +26480,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -25671,7 +26604,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -25781,7 +26714,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -25893,7 +26826,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -25998,7 +26931,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -26109,7 +27042,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -26214,7 +27147,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -26325,7 +27258,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -26430,7 +27363,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -26541,7 +27474,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -26678,7 +27611,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -26801,7 +27734,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -26919,7 +27852,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -27029,7 +27962,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -27170,7 +28103,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -27243,7 +28176,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -27323,7 +28256,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -27429,7 +28362,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -27512,7 +28445,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -27644,7 +28577,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -27717,7 +28650,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -27795,7 +28728,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -27853,6 +28786,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -27881,7 +28822,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -27912,7 +28853,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -27940,7 +28882,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28022,6 +28965,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28053,7 +29002,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -28082,7 +29031,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28141,6 +29091,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -28175,7 +29131,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -28240,6 +29196,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28271,7 +29233,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -28330,6 +29292,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28361,7 +29329,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -28427,6 +29395,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -28455,7 +29431,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -28486,7 +29462,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -28561,6 +29538,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28592,7 +29575,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -28667,6 +29650,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28693,7 +29682,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -28747,6 +29736,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -28777,7 +29781,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -28857,6 +29861,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28890,7 +29900,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -28970,6 +29980,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -30036,7 +31052,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30117,7 +31133,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30202,7 +31218,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30263,7 +31279,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30335,7 +31351,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -35059,6 +36075,35 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "specificationList": { "description": "Specifications List", "type": "object", @@ -41477,6 +42522,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 55911e9556..4d0e75c6fa 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -4948,6 +4948,419 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/{databaseId}\/collections\/{collectionId}\/documents": { "get": { "summary": "List documents", @@ -5029,6 +5442,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -5088,7 +5509,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5173,6 +5595,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5269,6 +5697,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -5328,7 +5764,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -5406,6 +5843,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -5514,6 +5957,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5593,6 +6042,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -5702,6 +6166,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5814,6 +6284,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -5845,7 +6321,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -5918,7 +6394,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -6035,7 +6511,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -7549,6 +8025,419 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/{databaseId}\/tables\/{tableId}\/rows": { "get": { "summary": "List rows", @@ -7573,7 +8462,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -7629,6 +8518,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -7657,7 +8554,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -7687,7 +8584,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7768,6 +8666,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -7799,7 +8703,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -7863,6 +8767,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -7891,7 +8803,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -7921,7 +8833,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -7994,6 +8907,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8025,7 +8944,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -8098,6 +9017,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8124,7 +9049,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -8176,6 +9101,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -8206,7 +9146,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -8284,6 +9224,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -8317,7 +9263,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -8395,6 +9341,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9931,6 +10883,35 @@ "localeCodes": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "row": { "description": "Row", "type": "object", @@ -11840,6 +12821,59 @@ "recoveryCode": true } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 6d5721c73b..c39987cbaf 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 508, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -5367,6 +5367,419 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/usage": { "get": { "summary": "Get databases usage stats", @@ -9525,6 +9938,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9584,7 +10005,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9615,7 +10037,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9699,6 +10122,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9759,7 +10188,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9821,6 +10251,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9920,6 +10356,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10010,6 +10452,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10106,6 +10554,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -10165,7 +10621,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -10243,6 +10700,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -10351,6 +10814,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10430,6 +10899,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -10629,6 +11113,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10741,6 +11231,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10974,7 +11470,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -11524,7 +12020,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -11596,7 +12092,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -11847,7 +12343,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -11896,7 +12392,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -11946,7 +12442,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 466, + "weight": 478, "cookies": false, "type": "", "demo": "functions\/list-templates.md", @@ -12040,7 +12536,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 465, + "weight": 477, "cookies": false, "type": "", "demo": "functions\/get-template.md", @@ -12098,7 +12594,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 459, + "weight": 471, "cookies": false, "type": "", "demo": "functions\/list-usage.md", @@ -12168,7 +12664,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -12227,7 +12723,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -12474,7 +12970,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -12535,7 +13031,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -12612,7 +13108,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -12692,7 +13188,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -12784,7 +13280,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -12869,7 +13365,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -12975,7 +13471,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -13071,7 +13567,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -13133,7 +13629,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -13200,7 +13696,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -13285,7 +13781,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -13352,7 +13848,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -13425,7 +13921,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -13542,7 +14038,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -13606,7 +14102,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -13673,7 +14169,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 458, + "weight": 470, "cookies": false, "type": "", "demo": "functions\/get-usage.md", @@ -13751,7 +14247,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -13810,7 +14306,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -13900,7 +14396,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -13967,7 +14463,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -14059,7 +14555,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -16423,7 +16919,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": "", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -16620,7 +17116,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -21355,7 +21851,7 @@ "type": "string", "description": "Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "internalFile": { "type": "boolean", @@ -22590,7 +23086,7 @@ "x-appwrite": { "method": "list", "group": "projects", - "weight": 436, + "weight": 448, "cookies": false, "type": "", "demo": "projects\/list.md", @@ -24233,7 +24729,7 @@ "x-appwrite": { "method": "listDevKeys", "group": "devKeys", - "weight": 434, + "weight": 446, "cookies": false, "type": "", "demo": "projects\/list-dev-keys.md", @@ -24303,7 +24799,7 @@ "x-appwrite": { "method": "createDevKey", "group": "devKeys", - "weight": 431, + "weight": 443, "cookies": false, "type": "", "demo": "projects\/create-dev-key.md", @@ -24386,7 +24882,7 @@ "x-appwrite": { "method": "getDevKey", "group": "devKeys", - "weight": 433, + "weight": 445, "cookies": false, "type": "", "demo": "projects\/get-dev-key.md", @@ -24452,7 +24948,7 @@ "x-appwrite": { "method": "updateDevKey", "group": "devKeys", - "weight": 432, + "weight": 444, "cookies": false, "type": "", "demo": "projects\/update-dev-key.md", @@ -24538,7 +25034,7 @@ "x-appwrite": { "method": "deleteDevKey", "group": "devKeys", - "weight": 435, + "weight": 447, "cookies": false, "type": "", "demo": "projects\/delete-dev-key.md", @@ -28351,7 +28847,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 514, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28920,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 509, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28990,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 511, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +29073,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 512, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +29193,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 510, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +29274,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 513, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +29327,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 515, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +29387,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 516, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -28949,7 +29445,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -29021,7 +29517,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -29288,7 +29784,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -29337,7 +29833,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29387,7 +29883,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 503, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29481,7 +29977,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 504, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29539,7 +30035,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 505, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -29609,7 +30105,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -29668,7 +30164,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -29930,7 +30426,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -29991,7 +30487,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -30068,7 +30564,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -30148,7 +30644,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -30248,7 +30744,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -30327,7 +30823,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -30433,7 +30929,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -30530,7 +31026,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -30592,7 +31088,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -30659,7 +31155,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -30744,7 +31240,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -30811,7 +31307,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30882,7 +31378,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30946,7 +31442,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31013,7 +31509,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 506, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31091,7 +31587,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31150,7 +31646,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31240,7 +31736,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31307,7 +31803,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31399,7 +31895,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -32833,7 +33329,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -32905,7 +33401,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -32963,6 +33459,419 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/usage": { "get": { "summary": "Get TablesDB usage stats", @@ -32987,7 +33896,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 378, + "weight": 384, "cookies": false, "type": "", "demo": "tablesdb\/list-usage.md", @@ -33082,7 +33991,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -33141,7 +34050,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -33219,7 +34128,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -33278,7 +34187,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -33361,7 +34270,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -33469,7 +34378,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -33539,7 +34448,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -33643,7 +34552,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -33713,7 +34622,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -33797,7 +34706,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -33906,7 +34815,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -34017,7 +34926,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -34126,7 +35035,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -34237,7 +35146,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -34346,7 +35255,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -34457,7 +35366,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -34576,7 +35485,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -34697,7 +35606,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -34818,7 +35727,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -34941,7 +35850,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -35062,7 +35971,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -35185,7 +36094,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -35294,7 +36203,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -35405,7 +36314,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -35509,7 +36418,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -35619,7 +36528,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -35723,7 +36632,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -35833,7 +36742,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -35937,7 +36846,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -36047,7 +36956,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -36183,7 +37092,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -36305,7 +37214,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -36422,7 +37331,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -36531,7 +37440,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -36671,7 +37580,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -36743,7 +37652,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -36822,7 +37731,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -36927,7 +37836,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -37009,7 +37918,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -37140,7 +38049,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -37212,7 +38121,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -37289,7 +38198,7 @@ "x-appwrite": { "method": "listTableLogs", "group": "tables", - "weight": 384, + "weight": 390, "cookies": false, "type": "", "demo": "tablesdb\/list-table-logs.md", @@ -37370,7 +38279,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -37426,6 +38335,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -37454,7 +38371,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -37484,7 +38401,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -37511,7 +38429,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37591,6 +38510,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37622,7 +38547,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -37650,7 +38575,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -37708,6 +38634,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -37742,7 +38674,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -37806,6 +38738,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37837,7 +38775,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -37895,6 +38833,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -37926,7 +38870,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -37990,6 +38934,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -38018,7 +38970,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -38048,7 +39000,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -38121,6 +39074,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38152,7 +39111,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -38225,6 +39184,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38251,7 +39216,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -38303,6 +39268,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -38331,7 +39311,7 @@ "x-appwrite": { "method": "listRowLogs", "group": "logs", - "weight": 428, + "weight": 434, "cookies": false, "type": "", "demo": "tablesdb\/list-row-logs.md", @@ -38422,7 +39402,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -38500,6 +39480,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38533,7 +39519,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -38611,6 +39597,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -38642,7 +39634,7 @@ "x-appwrite": { "method": "getTableUsage", "group": null, - "weight": 385, + "weight": 391, "cookies": false, "type": "", "demo": "tablesdb\/get-table-usage.md", @@ -38731,7 +39723,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 377, + "weight": 383, "cookies": false, "type": "", "demo": "tablesdb\/get-usage.md", @@ -39916,7 +40908,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -39996,7 +40988,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40080,7 +41072,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40140,7 +41132,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40211,7 +41203,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -46040,6 +47032,35 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "migrationList": { "description": "Migrations List", "type": "object", @@ -56554,6 +57575,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 98077f1050..7e0785fc0a 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -4893,6 +4893,431 @@ ] } }, + "\/databases\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "databasesListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 376, + "cookies": false, + "type": "", + "demo": "databases\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "databasesCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 372, + "cookies": false, + "type": "", + "demo": "databases\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/databases\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "databasesGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 373, + "cookies": false, + "type": "", + "demo": "databases\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "databasesUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 374, + "cookies": false, + "type": "", + "demo": "databases\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "databasesDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "databases" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 375, + "cookies": false, + "type": "", + "demo": "databases\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/databases\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "databasesCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "databases" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 377, + "cookies": false, + "type": "", + "demo": "databases\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/databases\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/databases\/{databaseId}": { "get": { "summary": "Get database", @@ -8993,6 +9418,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9053,7 +9486,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9085,7 +9519,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9171,6 +9606,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9232,7 +9673,8 @@ "parameters": [ "databaseId", "collectionId", - "documents" + "documents", + "transactionId" ], "required": [ "databaseId", @@ -9295,6 +9737,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9395,6 +9843,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9486,6 +9940,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9584,6 +10044,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -9644,7 +10112,8 @@ "collectionId", "documentId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -9724,6 +10193,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -9834,6 +10309,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -9915,6 +10396,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -10026,6 +10522,12 @@ "description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10140,6 +10642,12 @@ "description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -10375,7 +10883,7 @@ "tags": [ "databases" ], - "description": "Get index by ID.", + "description": "Get an index by its unique ID.", "responses": { "200": { "description": "Index", @@ -10541,7 +11049,7 @@ "x-appwrite": { "method": "list", "group": "functions", - "weight": 440, + "weight": 452, "cookies": false, "type": "", "demo": "functions\/list.md", @@ -10614,7 +11122,7 @@ "x-appwrite": { "method": "create", "group": "functions", - "weight": 437, + "weight": 449, "cookies": false, "type": "", "demo": "functions\/create.md", @@ -10866,7 +11374,7 @@ "x-appwrite": { "method": "listRuntimes", "group": "runtimes", - "weight": 442, + "weight": 454, "cookies": false, "type": "", "demo": "functions\/list-runtimes.md", @@ -10916,7 +11424,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "runtimes", - "weight": 443, + "weight": 455, "cookies": false, "type": "", "demo": "functions\/list-specifications.md", @@ -10967,7 +11475,7 @@ "x-appwrite": { "method": "get", "group": "functions", - "weight": 438, + "weight": 450, "cookies": false, "type": "", "demo": "functions\/get.md", @@ -11027,7 +11535,7 @@ "x-appwrite": { "method": "update", "group": "functions", - "weight": 439, + "weight": 451, "cookies": false, "type": "", "demo": "functions\/update.md", @@ -11275,7 +11783,7 @@ "x-appwrite": { "method": "delete", "group": "functions", - "weight": 441, + "weight": 453, "cookies": false, "type": "", "demo": "functions\/delete.md", @@ -11337,7 +11845,7 @@ "x-appwrite": { "method": "updateFunctionDeployment", "group": "functions", - "weight": 446, + "weight": 458, "cookies": false, "type": "", "demo": "functions\/update-function-deployment.md", @@ -11415,7 +11923,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 447, + "weight": 459, "cookies": false, "type": "", "demo": "functions\/list-deployments.md", @@ -11496,7 +12004,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 444, + "weight": 456, "cookies": false, "type": "upload", "demo": "functions\/create-deployment.md", @@ -11589,7 +12097,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 452, + "weight": 464, "cookies": false, "type": "", "demo": "functions\/create-duplicate-deployment.md", @@ -11675,7 +12183,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 449, + "weight": 461, "cookies": false, "type": "", "demo": "functions\/create-template-deployment.md", @@ -11782,7 +12290,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 450, + "weight": 462, "cookies": false, "type": "", "demo": "functions\/create-vcs-deployment.md", @@ -11879,7 +12387,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 445, + "weight": 457, "cookies": false, "type": "", "demo": "functions\/get-deployment.md", @@ -11942,7 +12450,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 448, + "weight": 460, "cookies": false, "type": "", "demo": "functions\/delete-deployment.md", @@ -12010,7 +12518,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 451, + "weight": 463, "cookies": false, "type": "location", "demo": "functions\/get-deployment-download.md", @@ -12096,7 +12604,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 453, + "weight": 465, "cookies": false, "type": "", "demo": "functions\/update-deployment-status.md", @@ -12164,7 +12672,7 @@ "x-appwrite": { "method": "listExecutions", "group": "executions", - "weight": 456, + "weight": 468, "cookies": false, "type": "", "demo": "functions\/list-executions.md", @@ -12239,7 +12747,7 @@ "x-appwrite": { "method": "createExecution", "group": "executions", - "weight": 454, + "weight": 466, "cookies": false, "type": "", "demo": "functions\/create-execution.md", @@ -12358,7 +12866,7 @@ "x-appwrite": { "method": "getExecution", "group": "executions", - "weight": 455, + "weight": 467, "cookies": false, "type": "", "demo": "functions\/get-execution.md", @@ -12424,7 +12932,7 @@ "x-appwrite": { "method": "deleteExecution", "group": "executions", - "weight": 457, + "weight": 469, "cookies": false, "type": "", "demo": "functions\/delete-execution.md", @@ -12492,7 +13000,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 462, + "weight": 474, "cookies": false, "type": "", "demo": "functions\/list-variables.md", @@ -12552,7 +13060,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 460, + "weight": 472, "cookies": false, "type": "", "demo": "functions\/create-variable.md", @@ -12643,7 +13151,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 461, + "weight": 473, "cookies": false, "type": "", "demo": "functions\/get-variable.md", @@ -12711,7 +13219,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 463, + "weight": 475, "cookies": false, "type": "", "demo": "functions\/update-variable.md", @@ -12804,7 +13312,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 464, + "weight": 476, "cookies": false, "type": "", "demo": "functions\/delete-variable.md", @@ -15215,7 +15723,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": "", - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -15413,7 +15921,7 @@ "type": "string", "description": "Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as :.", "default": null, - "x-example": "[ID1:ID2]" + "x-example": "" }, "icon": { "type": "string", @@ -19909,7 +20417,7 @@ "x-appwrite": { "method": "list", "group": "sites", - "weight": 469, + "weight": 481, "cookies": false, "type": "", "demo": "sites\/list.md", @@ -19982,7 +20490,7 @@ "x-appwrite": { "method": "create", "group": "sites", - "weight": 467, + "weight": 479, "cookies": false, "type": "", "demo": "sites\/create.md", @@ -20250,7 +20758,7 @@ "x-appwrite": { "method": "listFrameworks", "group": "frameworks", - "weight": 472, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-frameworks.md", @@ -20300,7 +20808,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 507, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -20351,7 +20859,7 @@ "x-appwrite": { "method": "get", "group": "sites", - "weight": 468, + "weight": 480, "cookies": false, "type": "", "demo": "sites\/get.md", @@ -20411,7 +20919,7 @@ "x-appwrite": { "method": "update", "group": "sites", - "weight": 470, + "weight": 482, "cookies": false, "type": "", "demo": "sites\/update.md", @@ -20674,7 +21182,7 @@ "x-appwrite": { "method": "delete", "group": "sites", - "weight": 471, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/delete.md", @@ -20736,7 +21244,7 @@ "x-appwrite": { "method": "updateSiteDeployment", "group": "sites", - "weight": 478, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-site-deployment.md", @@ -20814,7 +21322,7 @@ "x-appwrite": { "method": "listDeployments", "group": "deployments", - "weight": 477, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-deployments.md", @@ -20895,7 +21403,7 @@ "x-appwrite": { "method": "createDeployment", "group": "deployments", - "weight": 473, + "weight": 485, "cookies": false, "type": "upload", "demo": "sites\/create-deployment.md", @@ -20996,7 +21504,7 @@ "x-appwrite": { "method": "createDuplicateDeployment", "group": "deployments", - "weight": 481, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/create-duplicate-deployment.md", @@ -21076,7 +21584,7 @@ "x-appwrite": { "method": "createTemplateDeployment", "group": "deployments", - "weight": 474, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-template-deployment.md", @@ -21183,7 +21691,7 @@ "x-appwrite": { "method": "createVcsDeployment", "group": "deployments", - "weight": 475, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-vcs-deployment.md", @@ -21281,7 +21789,7 @@ "x-appwrite": { "method": "getDeployment", "group": "deployments", - "weight": 476, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-deployment.md", @@ -21344,7 +21852,7 @@ "x-appwrite": { "method": "deleteDeployment", "group": "deployments", - "weight": 479, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-deployment.md", @@ -21412,7 +21920,7 @@ "x-appwrite": { "method": "getDeploymentDownload", "group": "deployments", - "weight": 480, + "weight": 492, "cookies": false, "type": "location", "demo": "sites\/get-deployment-download.md", @@ -21498,7 +22006,7 @@ "x-appwrite": { "method": "updateDeploymentStatus", "group": "deployments", - "weight": 482, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/update-deployment-status.md", @@ -21566,7 +22074,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21638,7 +22146,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21703,7 +22211,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 497, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21771,7 +22279,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 500, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21831,7 +22339,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 498, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21922,7 +22430,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 499, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21990,7 +22498,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 501, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22083,7 +22591,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 502, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -23391,7 +23899,7 @@ "x-appwrite": { "method": "list", "group": "tablesdb", - "weight": 376, + "weight": 382, "cookies": false, "type": "", "demo": "tablesdb\/list.md", @@ -23464,7 +23972,7 @@ "x-appwrite": { "method": "create", "group": "tablesdb", - "weight": 372, + "weight": 378, "cookies": false, "type": "", "demo": "tablesdb\/create.md", @@ -23523,6 +24031,431 @@ ] } }, + "\/tablesdb\/transactions": { + "get": { + "summary": "List transactions", + "operationId": "tablesDBListTransactions", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "List transactions across all databases.", + "responses": { + "200": { + "description": "Transaction List", + "schema": { + "$ref": "#\/definitions\/transactionList" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "listTransactions", + "group": "transactions", + "weight": 441, + "cookies": false, + "type": "", + "demo": "tablesdb\/list-transactions.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/list-transactions.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "queries", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries).", + "required": false, + "type": "array", + "collectionFormat": "multi", + "items": { + "type": "string" + }, + "default": [], + "in": "query" + } + ] + }, + "post": { + "summary": "Create transaction", + "operationId": "tablesDBCreateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create a new transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createTransaction", + "group": "transactions", + "weight": 437, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "ttl": { + "type": "integer", + "description": "Seconds before the transaction expires.", + "default": 300, + "x-example": 60 + } + } + } + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}": { + "get": { + "summary": "Get transaction", + "operationId": "tablesDBGetTransaction", + "consumes": [], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Get a transaction by its unique ID.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "getTransaction", + "group": "transactions", + "weight": 438, + "cookies": false, + "type": "", + "demo": "tablesdb\/get-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/get-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.read", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + }, + "patch": { + "summary": "Update transaction", + "operationId": "tablesDBUpdateTransaction", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Update a transaction, to either commit or roll back its operations.", + "responses": { + "200": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "updateTransaction", + "group": "transactions", + "weight": 439, + "cookies": false, + "type": "", + "demo": "tablesdb\/update-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/update-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "boolean", + "description": "Commit transaction?", + "default": false, + "x-example": false + }, + "rollback": { + "type": "boolean", + "description": "Rollback transaction?", + "default": false, + "x-example": false + } + } + } + } + ] + }, + "delete": { + "summary": "Delete transaction", + "operationId": "tablesDBDeleteTransaction", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "tablesDB" + ], + "description": "Delete a transaction by its unique ID.", + "responses": { + "204": { + "description": "No content" + } + }, + "deprecated": false, + "x-appwrite": { + "method": "deleteTransaction", + "group": "transactions", + "weight": 440, + "cookies": false, + "type": "", + "demo": "tablesdb\/delete-transaction.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/delete-transaction.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + } + ] + } + }, + "\/tablesdb\/transactions\/{transactionId}\/operations": { + "post": { + "summary": "Add operations to transaction", + "operationId": "tablesDBCreateOperations", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "tablesDB" + ], + "description": "Create multiple operations in a single transaction.", + "responses": { + "201": { + "description": "Transaction", + "schema": { + "$ref": "#\/definitions\/transaction" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createOperations", + "group": "transactions", + "weight": 442, + "cookies": false, + "type": "", + "demo": "tablesdb\/create-operations.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/tablesdb\/create-operations.md", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "transactions.write", + "platforms": [ + "server", + "client" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "Session": [], + "JWT": [] + } + ], + "parameters": [ + { + "name": "transactionId", + "description": "Transaction ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of staged operations.", + "default": [], + "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "items": { + "type": "object" + } + } + } + } + } + ] + } + }, "\/tablesdb\/{databaseId}": { "get": { "summary": "Get database", @@ -23547,7 +24480,7 @@ "x-appwrite": { "method": "get", "group": "tablesdb", - "weight": 373, + "weight": 379, "cookies": false, "type": "", "demo": "tablesdb\/get.md", @@ -23607,7 +24540,7 @@ "x-appwrite": { "method": "update", "group": "tablesdb", - "weight": 374, + "weight": 380, "cookies": false, "type": "", "demo": "tablesdb\/update.md", @@ -23686,7 +24619,7 @@ "x-appwrite": { "method": "delete", "group": "tablesdb", - "weight": 375, + "weight": 381, "cookies": false, "type": "", "demo": "tablesdb\/delete.md", @@ -23746,7 +24679,7 @@ "x-appwrite": { "method": "listTables", "group": "tables", - "weight": 383, + "weight": 389, "cookies": false, "type": "", "demo": "tablesdb\/list-tables.md", @@ -23830,7 +24763,7 @@ "x-appwrite": { "method": "createTable", "group": "tables", - "weight": 379, + "weight": 385, "cookies": false, "type": "", "demo": "tablesdb\/create-table.md", @@ -23939,7 +24872,7 @@ "x-appwrite": { "method": "getTable", "group": "tables", - "weight": 380, + "weight": 386, "cookies": false, "type": "", "demo": "tablesdb\/get-table.md", @@ -24010,7 +24943,7 @@ "x-appwrite": { "method": "updateTable", "group": "tables", - "weight": 381, + "weight": 387, "cookies": false, "type": "", "demo": "tablesdb\/update-table.md", @@ -24115,7 +25048,7 @@ "x-appwrite": { "method": "deleteTable", "group": "tables", - "weight": 382, + "weight": 388, "cookies": false, "type": "", "demo": "tablesdb\/delete-table.md", @@ -24186,7 +25119,7 @@ "x-appwrite": { "method": "listColumns", "group": "columns", - "weight": 388, + "weight": 394, "cookies": false, "type": "", "demo": "tablesdb\/list-columns.md", @@ -24271,7 +25204,7 @@ "x-appwrite": { "method": "createBooleanColumn", "group": "columns", - "weight": 389, + "weight": 395, "cookies": false, "type": "", "demo": "tablesdb\/create-boolean-column.md", @@ -24381,7 +25314,7 @@ "x-appwrite": { "method": "updateBooleanColumn", "group": "columns", - "weight": 390, + "weight": 396, "cookies": false, "type": "", "demo": "tablesdb\/update-boolean-column.md", @@ -24493,7 +25426,7 @@ "x-appwrite": { "method": "createDatetimeColumn", "group": "columns", - "weight": 391, + "weight": 397, "cookies": false, "type": "", "demo": "tablesdb\/create-datetime-column.md", @@ -24603,7 +25536,7 @@ "x-appwrite": { "method": "updateDatetimeColumn", "group": "columns", - "weight": 392, + "weight": 398, "cookies": false, "type": "", "demo": "tablesdb\/update-datetime-column.md", @@ -24715,7 +25648,7 @@ "x-appwrite": { "method": "createEmailColumn", "group": "columns", - "weight": 393, + "weight": 399, "cookies": false, "type": "", "demo": "tablesdb\/create-email-column.md", @@ -24825,7 +25758,7 @@ "x-appwrite": { "method": "updateEmailColumn", "group": "columns", - "weight": 394, + "weight": 400, "cookies": false, "type": "", "demo": "tablesdb\/update-email-column.md", @@ -24937,7 +25870,7 @@ "x-appwrite": { "method": "createEnumColumn", "group": "columns", - "weight": 395, + "weight": 401, "cookies": false, "type": "", "demo": "tablesdb\/create-enum-column.md", @@ -25057,7 +25990,7 @@ "x-appwrite": { "method": "updateEnumColumn", "group": "columns", - "weight": 396, + "weight": 402, "cookies": false, "type": "", "demo": "tablesdb\/update-enum-column.md", @@ -25179,7 +26112,7 @@ "x-appwrite": { "method": "createFloatColumn", "group": "columns", - "weight": 397, + "weight": 403, "cookies": false, "type": "", "demo": "tablesdb\/create-float-column.md", @@ -25301,7 +26234,7 @@ "x-appwrite": { "method": "updateFloatColumn", "group": "columns", - "weight": 398, + "weight": 404, "cookies": false, "type": "", "demo": "tablesdb\/update-float-column.md", @@ -25425,7 +26358,7 @@ "x-appwrite": { "method": "createIntegerColumn", "group": "columns", - "weight": 399, + "weight": 405, "cookies": false, "type": "", "demo": "tablesdb\/create-integer-column.md", @@ -25547,7 +26480,7 @@ "x-appwrite": { "method": "updateIntegerColumn", "group": "columns", - "weight": 400, + "weight": 406, "cookies": false, "type": "", "demo": "tablesdb\/update-integer-column.md", @@ -25671,7 +26604,7 @@ "x-appwrite": { "method": "createIpColumn", "group": "columns", - "weight": 401, + "weight": 407, "cookies": false, "type": "", "demo": "tablesdb\/create-ip-column.md", @@ -25781,7 +26714,7 @@ "x-appwrite": { "method": "updateIpColumn", "group": "columns", - "weight": 402, + "weight": 408, "cookies": false, "type": "", "demo": "tablesdb\/update-ip-column.md", @@ -25893,7 +26826,7 @@ "x-appwrite": { "method": "createLineColumn", "group": "columns", - "weight": 403, + "weight": 409, "cookies": false, "type": "", "demo": "tablesdb\/create-line-column.md", @@ -25998,7 +26931,7 @@ "x-appwrite": { "method": "updateLineColumn", "group": "columns", - "weight": 404, + "weight": 410, "cookies": false, "type": "", "demo": "tablesdb\/update-line-column.md", @@ -26109,7 +27042,7 @@ "x-appwrite": { "method": "createPointColumn", "group": "columns", - "weight": 405, + "weight": 411, "cookies": false, "type": "", "demo": "tablesdb\/create-point-column.md", @@ -26214,7 +27147,7 @@ "x-appwrite": { "method": "updatePointColumn", "group": "columns", - "weight": 406, + "weight": 412, "cookies": false, "type": "", "demo": "tablesdb\/update-point-column.md", @@ -26325,7 +27258,7 @@ "x-appwrite": { "method": "createPolygonColumn", "group": "columns", - "weight": 407, + "weight": 413, "cookies": false, "type": "", "demo": "tablesdb\/create-polygon-column.md", @@ -26430,7 +27363,7 @@ "x-appwrite": { "method": "updatePolygonColumn", "group": "columns", - "weight": 408, + "weight": 414, "cookies": false, "type": "", "demo": "tablesdb\/update-polygon-column.md", @@ -26541,7 +27474,7 @@ "x-appwrite": { "method": "createRelationshipColumn", "group": "columns", - "weight": 409, + "weight": 415, "cookies": false, "type": "", "demo": "tablesdb\/create-relationship-column.md", @@ -26678,7 +27611,7 @@ "x-appwrite": { "method": "createStringColumn", "group": "columns", - "weight": 411, + "weight": 417, "cookies": false, "type": "", "demo": "tablesdb\/create-string-column.md", @@ -26801,7 +27734,7 @@ "x-appwrite": { "method": "updateStringColumn", "group": "columns", - "weight": 412, + "weight": 418, "cookies": false, "type": "", "demo": "tablesdb\/update-string-column.md", @@ -26919,7 +27852,7 @@ "x-appwrite": { "method": "createUrlColumn", "group": "columns", - "weight": 413, + "weight": 419, "cookies": false, "type": "", "demo": "tablesdb\/create-url-column.md", @@ -27029,7 +27962,7 @@ "x-appwrite": { "method": "updateUrlColumn", "group": "columns", - "weight": 414, + "weight": 420, "cookies": false, "type": "", "demo": "tablesdb\/update-url-column.md", @@ -27170,7 +28103,7 @@ "x-appwrite": { "method": "getColumn", "group": "columns", - "weight": 386, + "weight": 392, "cookies": false, "type": "", "demo": "tablesdb\/get-column.md", @@ -27243,7 +28176,7 @@ "x-appwrite": { "method": "deleteColumn", "group": "columns", - "weight": 387, + "weight": 393, "cookies": false, "type": "", "demo": "tablesdb\/delete-column.md", @@ -27323,7 +28256,7 @@ "x-appwrite": { "method": "updateRelationshipColumn", "group": "columns", - "weight": 410, + "weight": 416, "cookies": false, "type": "", "demo": "tablesdb\/update-relationship-column.md", @@ -27429,7 +28362,7 @@ "x-appwrite": { "method": "listIndexes", "group": "indexes", - "weight": 418, + "weight": 424, "cookies": false, "type": "", "demo": "tablesdb\/list-indexes.md", @@ -27512,7 +28445,7 @@ "x-appwrite": { "method": "createIndex", "group": "indexes", - "weight": 415, + "weight": 421, "cookies": false, "type": "", "demo": "tablesdb\/create-index.md", @@ -27644,7 +28577,7 @@ "x-appwrite": { "method": "getIndex", "group": "indexes", - "weight": 416, + "weight": 422, "cookies": false, "type": "", "demo": "tablesdb\/get-index.md", @@ -27717,7 +28650,7 @@ "x-appwrite": { "method": "deleteIndex", "group": "indexes", - "weight": 417, + "weight": 423, "cookies": false, "type": "", "demo": "tablesdb\/delete-index.md", @@ -27795,7 +28728,7 @@ "x-appwrite": { "method": "listRows", "group": "rows", - "weight": 427, + "weight": 433, "cookies": false, "type": "", "demo": "tablesdb\/list-rows.md", @@ -27853,6 +28786,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -27881,7 +28822,7 @@ "x-appwrite": { "method": "createRow", "group": "rows", - "weight": 419, + "weight": 425, "cookies": false, "type": "", "demo": "tablesdb\/create-row.md", @@ -27912,7 +28853,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -27940,7 +28882,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28022,6 +28965,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28053,7 +29002,7 @@ "x-appwrite": { "method": "upsertRows", "group": "rows", - "weight": 424, + "weight": 430, "cookies": false, "type": "", "demo": "tablesdb\/upsert-rows.md", @@ -28082,7 +29031,8 @@ "parameters": [ "databaseId", "tableId", - "rows" + "rows", + "transactionId" ], "required": [ "databaseId", @@ -28141,6 +29091,12 @@ "items": { "type": "object" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } }, "required": [ @@ -28175,7 +29131,7 @@ "x-appwrite": { "method": "updateRows", "group": "rows", - "weight": 422, + "weight": 428, "cookies": false, "type": "", "demo": "tablesdb\/update-rows.md", @@ -28240,6 +29196,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28271,7 +29233,7 @@ "x-appwrite": { "method": "deleteRows", "group": "rows", - "weight": 426, + "weight": 432, "cookies": false, "type": "", "demo": "tablesdb\/delete-rows.md", @@ -28330,6 +29292,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28361,7 +29329,7 @@ "x-appwrite": { "method": "getRow", "group": "rows", - "weight": 420, + "weight": 426, "cookies": false, "type": "", "demo": "tablesdb\/get-row.md", @@ -28427,6 +29395,14 @@ }, "default": [], "in": "query" + }, + { + "name": "transactionId", + "description": "Transaction ID to read uncommitted changes within the transaction.", + "required": false, + "type": "string", + "x-example": "", + "in": "query" } ] }, @@ -28455,7 +29431,7 @@ "x-appwrite": { "method": "upsertRow", "group": "rows", - "weight": 423, + "weight": 429, "cookies": false, "type": "", "demo": "tablesdb\/upsert-row.md", @@ -28486,7 +29462,8 @@ "tableId", "rowId", "data", - "permissions" + "permissions", + "transactionId" ], "required": [ "databaseId", @@ -28561,6 +29538,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28592,7 +29575,7 @@ "x-appwrite": { "method": "updateRow", "group": "rows", - "weight": 421, + "weight": 427, "cookies": false, "type": "", "demo": "tablesdb\/update-row.md", @@ -28667,6 +29650,12 @@ "items": { "type": "string" } + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28693,7 +29682,7 @@ "x-appwrite": { "method": "deleteRow", "group": "rows", - "weight": 425, + "weight": 431, "cookies": false, "type": "", "demo": "tablesdb\/delete-row.md", @@ -28747,6 +29736,21 @@ "type": "string", "x-example": "", "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" + } + } + } } ] } @@ -28777,7 +29781,7 @@ "x-appwrite": { "method": "decrementRowColumn", "group": "rows", - "weight": 430, + "weight": 436, "cookies": false, "type": "", "demo": "tablesdb\/decrement-row-column.md", @@ -28857,6 +29861,12 @@ "description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -28890,7 +29900,7 @@ "x-appwrite": { "method": "incrementRowColumn", "group": "rows", - "weight": 429, + "weight": 435, "cookies": false, "type": "", "demo": "tablesdb\/increment-row-column.md", @@ -28970,6 +29980,12 @@ "description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.", "default": null, "x-example": null + }, + "transactionId": { + "type": "string", + "description": "Transaction ID for staging the operation.", + "default": null, + "x-example": "" } } } @@ -30036,7 +31052,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 519, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30117,7 +31133,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 517, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30202,7 +31218,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 518, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30263,7 +31279,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 520, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30335,7 +31351,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 521, "cookies": false, "type": "", "demo": "tokens\/delete.md", @@ -35059,6 +36075,35 @@ "targets": "" } }, + "transactionList": { + "description": "Transaction List", + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of transactions that matched your query.", + "x-example": 5, + "format": "int32" + }, + "transactions": { + "type": "array", + "description": "List of transactions.", + "items": { + "type": "object", + "$ref": "#\/definitions\/transaction" + }, + "x-example": "" + } + }, + "required": [ + "total", + "transactions" + ], + "example": { + "total": 5, + "transactions": "" + } + }, "specificationList": { "description": "Specifications List", "type": "object", @@ -41477,6 +42522,59 @@ "subscribe": "users" } }, + "transaction": { + "description": "Transaction", + "type": "object", + "properties": { + "$id": { + "type": "string", + "description": "Transaction ID.", + "x-example": "259125845563242502" + }, + "$createdAt": { + "type": "string", + "description": "Transaction creation time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "$updatedAt": { + "type": "string", + "description": "Transaction update date in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + }, + "status": { + "type": "string", + "description": "Current status of the transaction. One of: pending, committing, committed, rolled_back, failed.", + "x-example": "pending" + }, + "operations": { + "type": "integer", + "description": "Number of operations in the transaction.", + "x-example": 5, + "format": "int32" + }, + "expiresAt": { + "type": "string", + "description": "Expiration time in ISO 8601 format.", + "x-example": "2020-10-15T06:38:00.000+00:00" + } + }, + "required": [ + "$id", + "$createdAt", + "$updatedAt", + "status", + "operations", + "expiresAt" + ], + "example": { + "$id": "259125845563242502", + "$createdAt": "2020-10-15T06:38:00.000+00:00", + "$updatedAt": "2020-10-15T06:38:00.000+00:00", + "status": "pending", + "operations": 5, + "expiresAt": "2020-10-15T06:38:00.000+00:00" + } + }, "subscriber": { "description": "Subscriber", "type": "object", diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index c9c2135ab3..3dd1faf175 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -8,6 +8,7 @@ use Appwrite\SDK\MethodType; use Appwrite\SDK\Response; use Appwrite\SDK\Specification\Format; use Appwrite\Template\Template; +use Appwrite\Utopia\Database\Validator\Operation; use Appwrite\Utopia\Response\Model; use Utopia\Database\Database; use Utopia\Database\Helpers\Permission; @@ -396,7 +397,37 @@ class OpenAPI3 extends Format $validator = $validator->getValidator(); } - switch ((!empty($validator)) ? \get_class($validator) : '') { + $class = !empty($validator) + ? \get_class($validator) + : ''; + + $base = !empty($class) + ? \get_parent_class($class) + : ''; + + switch ($base) { + case 'Appwrite\Utopia\Database\Validator\Queries\Base': + $class = $base; + break; + } + + if ($class === 'Utopia\Validator\AnyOf') { + $validator = $param['validator']->getValidators()[0]; + $class = \get_class($validator); + } + + $array = false; + if ($class === 'Utopia\Validator\ArrayList') { + $array = true; + $subclass = \get_class($validator->getValidator()); + switch ($subclass) { + case 'Appwrite\Utopia\Database\Validator\Operation': + $class = $subclass; + break; + } + } + + switch ($class) { case 'Utopia\Database\Validator\UID': case 'Utopia\Validator\Text': $node['schema']['type'] = $validator->getType(); @@ -418,6 +449,20 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'datetime'; $node['schema']['x-example'] = Model::TYPE_DATETIME_EXAMPLE; break; + case 'Utopia\Database\Validator\Spatial': + /** @var Spatial $validator */ + $node['schema']['type'] = 'array'; + $node['schema']['items'] = [ + 'oneOf' => [ + ['type' => 'array'] + ] + ]; + $node['schema']['x-example'] = match ($validator->getSpatialType()) { + Database::VAR_POINT => '[1, 2]', + Database::VAR_LINESTRING => '[[1, 2], [3, 4], [5, 6]]', + Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', + }; + break; case 'Appwrite\Network\Validator\Email': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'email'; @@ -449,20 +494,7 @@ class OpenAPI3 extends Format 'type' => $validator->getValidator()->getType(), ]; break; - case 'Utopia\Database\Validator\Spatial': - /** @var Spatial $validator */ - $node['schema']['type'] = 'array'; - $node['schema']['items'] = [ - 'oneOf' => [ - ['type' => 'array'] - ] - ]; - $node['schema']['x-example'] = match ($validator->getSpatialType()) { - Database::VAR_POINT => '[1, 2]', - Database::VAR_LINESTRING => '[[1, 2], [3, 4], [5, 6]]', - Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', - }; - break; + case 'Appwrite\Utopia\Database\Validator\Queries\Base': case 'Appwrite\Utopia\Database\Validator\Queries\Columns': case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': @@ -556,8 +588,7 @@ class OpenAPI3 extends Format break; } } - - if ($allowed) { + if ($allowed && $validator->getType() === 'string') { $node['schema']['enum'] = $validator->getList(); $node['schema']['x-enum-name'] = $this->getEnumName($sdk->getNamespace() ?? '', $methodName, $name); $node['schema']['x-enum-keys'] = $this->getEnumKeys($sdk->getNamespace() ?? '', $methodName, $name); @@ -568,7 +599,35 @@ class OpenAPI3 extends Format break; case 'Appwrite\Utopia\Database\Validator\CompoundUID': $node['schema']['type'] = $validator->getType(); - $node['schema']['x-example'] = '[ID1:ID2]'; + $node['schema']['x-example'] = ''; + break; + case 'Appwrite\Utopia\Database\Validator\Operation': + if ($array) { + $validator = $validator->getValidator(); + } + + /** @var Operation $validator */ + $collectionIdKey = $validator->getCollectionIdKey(); + $documentIdKey = $validator->getDocumentIdKey(); + if ($array) { + $node['schema']['type'] = 'array'; + $node['schema']['items'] = ['type' => 'object']; + } else { + $node['schema']['type'] = 'object'; + } + $example = [ + 'action' => 'create', + 'databaseId' => '', + $collectionIdKey => '<'.\strtoupper(Template::fromCamelCaseToSnake($collectionIdKey)).'>', + $documentIdKey => '<'.\strtoupper(Template::fromCamelCaseToSnake($documentIdKey)).'>', + 'data' => [ + 'name' => 'Walter O\'Brien', + ], + ]; + if ($array) { + $example = [$example]; + } + $node['schema']['x-example'] = \str_replace("\n", "\n\t", \json_encode($example, JSON_PRETTY_PRINT)); break; default: $node['schema']['type'] = 'string'; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index a923f40ffa..ffa74ecd22 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -8,6 +8,7 @@ use Appwrite\SDK\MethodType; use Appwrite\SDK\Response; use Appwrite\SDK\Specification\Format; use Appwrite\Template\Template; +use Appwrite\Utopia\Database\Validator\Operation; use Appwrite\Utopia\Response\Model; use Utopia\Database\Database; use Utopia\Database\Helpers\Permission; @@ -422,6 +423,17 @@ class Swagger2 extends Format $class = \get_class($validator); } + $array = false; + if ($class === 'Utopia\Validator\ArrayList') { + $array = true; + $subclass = \get_class($validator->getValidator()); + switch ($subclass) { + case 'Appwrite\Utopia\Database\Validator\Operation': + $class = $subclass; + break; + } + } + switch ($class) { case 'Utopia\Validator\Text': case 'Utopia\Database\Validator\UID': @@ -444,6 +456,20 @@ class Swagger2 extends Format $node['format'] = 'datetime'; $node['x-example'] = Model::TYPE_DATETIME_EXAMPLE; break; + case 'Utopia\Database\Validator\Spatial': + /** @var Spatial $validator */ + $node['type'] = 'array'; + $node['schema']['items'] = [ + 'oneOf' => [ + ['type' => 'array'] + ] + ]; + $node['x-example'] = match ($validator->getSpatialType()) { + Database::VAR_POINT => '[1, 2]', + Database::VAR_LINESTRING => '[[1, 2], [3, 4], [5, 6]]', + Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', + }; + break; case 'Appwrite\Network\Validator\Email': $node['type'] = $validator->getType(); $node['format'] = 'email'; @@ -464,20 +490,6 @@ class Swagger2 extends Format 'type' => $validator->getValidator()->getType(), ]; break; - case 'Utopia\Database\Validator\Spatial': - /** @var Spatial $validator */ - $node['type'] = 'array'; - $node['schema']['items'] = [ - 'oneOf' => [ - ['type' => 'array'] - ] - ]; - $node['x-example'] = match ($validator->getSpatialType()) { - Database::VAR_POINT => '[1, 2]', - Database::VAR_LINESTRING => '[[1, 2], [3, 4], [5, 6]]', - Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', - }; - break; case 'Utopia\Validator\JSON': case 'Utopia\Validator\Mock': case 'Utopia\Validator\Assoc': @@ -562,20 +574,47 @@ class Swagger2 extends Format break; } } - if ($allowed && $validator->getType() === 'string') { $node['enum'] = $validator->getList(); $node['x-enum-name'] = $this->getEnumName($namespace, $methodName, $name); $node['x-enum-keys'] = $this->getEnumKeys($namespace, $methodName, $name); } - if ($validator->getType() === 'integer') { $node['format'] = 'int32'; } break; case 'Appwrite\Utopia\Database\Validator\CompoundUID': $node['type'] = $validator->getType(); - $node['x-example'] = '[ID1:ID2]'; + $node['x-example'] = ''; + break; + case 'Appwrite\Utopia\Database\Validator\Operation': + if ($array) { + $validator = $validator->getValidator(); + } + + /** @var Operation $validator */ + $collectionIdKey = $validator->getCollectionIdKey(); + $documentIdKey = $validator->getDocumentIdKey(); + if ($array) { + $node['type'] = 'array'; + $node['collectionFormat'] = 'multi'; + $node['items'] = ['type' => 'object']; + } else { + $node['type'] = 'object'; + } + $example = [ + 'action' => 'create', + 'databaseId' => '', + $collectionIdKey => '<'.\strtoupper(Template::fromCamelCaseToSnake($collectionIdKey)).'>', + $documentIdKey => '<'.\strtoupper(Template::fromCamelCaseToSnake($documentIdKey)).'>', + 'data' => [ + 'name' => 'Walter O\'Brien', + ], + ]; + if ($array) { + $example = [$example]; + } + $node['x-example'] = \str_replace("\n", "\n\t", \json_encode($example, JSON_PRETTY_PRINT)); break; default: $node['type'] = 'string'; diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index f3fe2375b0..4632678940 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -64,12 +64,12 @@ class Operation extends Validator return $this->description; } - public function getCollectionIdName(): string + public function getCollectionIdKey(): string { return $this->collectionIdName; } - public function getDocumentIdName(): string + public function getDocumentIdKey(): string { return $this->documentIdName; } From ef685c5bc169307b0cf2dad6ad5477941d3784e3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Sep 2025 00:19:39 +1200 Subject: [PATCH 092/145] Fix tests --- .../Databases/Legacy/DatabasesBase.php | 140 +++++------------- .../Legacy/Transactions/TransactionsTest.php | 2 +- 2 files changed, 34 insertions(+), 108 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 0616f982b1..ecf6cfc1ad 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -4197,69 +4197,13 @@ trait DatabasesBase $this->assertCount(1, $documentsUser2['body']['documents']); } - public function testUniqueIndexDuplicate(): void + /** + * @depends testDefaultPermissions + */ + public function testUniqueIndexDuplicate(array $data): array { - // Setup: create database, collection, attribute, and initial document - $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' => 'Unique Index DB', - ]); - $databaseId = $database['body']['$id']; - $movies = $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' => 'Movies', - 'permissions' => [ - Permission::create(Role::user(ID::custom($this->getUser()['$id']))), - Permission::read(Role::user(ID::custom($this->getUser()['$id']))), - Permission::update(Role::user(ID::custom($this->getUser()['$id']))), - Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), - ], - 'documentSecurity' => true, - ]); - $moviesId = $movies['body']['$id']; - $title = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - $this->assertEquals(202, $title['headers']['status-code']); - sleep(2); - // Insert initial document - $doc1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Captain America', - 'releaseYear' => 1944, - 'actors' => [ - 'Chris Evans', - 'Samuel Jackson', - ] - ], - 'permissions' => [ - Permission::read(Role::user(ID::custom($this->getUser()['$id']))), - Permission::update(Role::user(ID::custom($this->getUser()['$id']))), - Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), - ] - ]); - $this->assertEquals(201, $doc1['headers']['status-code']); - - // Create unique index - $uniqueIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/indexes', array_merge([ + $databaseId = $data['databaseId']; + $uniqueIndex = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -4268,11 +4212,13 @@ trait DatabasesBase 'type' => 'unique', 'attributes' => ['title'], ]); + $this->assertEquals(202, $uniqueIndex['headers']['status-code']); + sleep(2); - // test for failure (duplicate title) - $duplicate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ + // test for failure + $duplicate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -4291,10 +4237,11 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); + $this->assertEquals(409, $duplicate['headers']['status-code']); // Test for exception when updating document to conflict - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -4313,9 +4260,11 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); + $this->assertEquals(201, $document['headers']['status-code']); - $duplicate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $document['body']['$id'], array_merge([ + // Test for exception when updating document to conflict + $duplicate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $document['body']['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -4334,48 +4283,17 @@ trait DatabasesBase Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), ] ]); + $this->assertEquals(409, $duplicate['headers']['status-code']); + + return $data; } - public function testPersistentCreatedAt(): void + /** + * @depends testUniqueIndexDuplicate + */ + public function testPersistentCreatedAt(array $data): array { - // Setup: create database, collection, attribute - $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' => 'CreatedAtDB', - ]); - $databaseId = $database['body']['$id']; - $movies = $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' => 'Movies', - 'permissions' => [ - Permission::create(Role::user(ID::custom($this->getUser()['$id']))), - Permission::read(Role::user(ID::custom($this->getUser()['$id']))), - Permission::update(Role::user(ID::custom($this->getUser()['$id']))), - Permission::delete(Role::user(ID::custom($this->getUser()['$id']))), - ], - 'documentSecurity' => true, - ]); - $moviesId = $movies['body']['$id']; - $title = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - $this->assertEquals(202, $title['headers']['status-code']); - sleep(2); $headers = $this->getSide() === 'client' ? array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -4385,31 +4303,37 @@ trait DatabasesBase 'x-appwrite-key' => $this->getProject()['apiKey'] ]; - $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', $headers, [ + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents', $headers, [ 'documentId' => ID::unique(), 'data' => [ 'title' => 'Creation Date Test', 'releaseYear' => 2000 ] ]); + $this->assertEquals($document['body']['title'], 'Creation Date Test'); + $documentId = $document['body']['$id']; $createdAt = $document['body']['$createdAt']; $updatedAt = $document['body']['$updatedAt']; \sleep(1); - $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $documentId, $headers, [ + + $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [ 'data' => [ 'title' => 'Updated Date Test', ] ]); + $updatedAtSecond = $document['body']['$updatedAt']; + $this->assertEquals($document['body']['title'], 'Updated Date Test'); $this->assertEquals($document['body']['$createdAt'], $createdAt); $this->assertNotEquals($document['body']['$updatedAt'], $updatedAt); \sleep(1); - $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $documentId, $headers, [ + + $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [ 'data' => [ 'title' => 'Again Updated Date Test', '$createdAt' => '2022-08-01 13:09:23.040', @@ -4426,6 +4350,8 @@ trait DatabasesBase $this->assertEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); } + + return $data; } public function testUpdatePermissionsWithEmptyPayload(): array diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php index d2017c4a08..34eb561d63 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php @@ -954,7 +954,7 @@ class TransactionsTest extends Scope 'commit' => true ]); - $this->assertEquals(409, $response['headers']['status-code']); // Conflict + $this->assertEquals(404, $response['headers']['status-code']); // Conflict } /** From 09753cd6dc1af932615b095c4f8a31f09ef80c56 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Sep 2025 01:16:23 +1200 Subject: [PATCH 093/145] Optimise schema --- app/config/collections/projects.php | 22 ++++------------------ src/Appwrite/Platform/Workers/Deletes.php | 5 ++--- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 7bd1d4a7d8..bf0cee3527 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -2530,7 +2530,7 @@ return [ [ '$id' => ID::custom('status'), 'type' => Database::VAR_STRING, - 'size' => 16, // pending | committing | committed | rolled_back | failed + 'size' => 16, // pending | committing | committed | failed 'signed' => true, 'required' => false, 'default' => 'pending', @@ -2560,14 +2560,7 @@ return [ ], 'indexes' => [ [ - '$id' => ID::custom('_key_status'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['status'], - 'lengths' => [], - 'orders' => [Database::ORDER_ASC], - ], - [ - '$id' => ID::custom('_key_expires'), + '$id' => ID::custom('_key_expiresAt'), 'type' => Database::INDEX_KEY, 'attributes' => ['expiresAt'], 'lengths' => [], @@ -2624,7 +2617,7 @@ return [ [ '$id' => ID::custom('action'), 'type' => Database::VAR_STRING, - 'size' => 32, // create | update | upsert | increment | decrement | delete + 'size' => 32, // create | update | upsert | increment | decrement | delete | bulkCreate | bulkUpdate | bulkUpsert | bulkDelete 'signed' => true, 'required' => true, 'default' => null, @@ -2634,7 +2627,7 @@ return [ [ '$id' => ID::custom('data'), 'type' => Database::VAR_STRING, - 'size' => 65535, + 'size' => 5_000_000, // Allow large payloads for bulk operations 'signed' => false, 'required' => true, 'default' => null, @@ -2650,13 +2643,6 @@ return [ 'lengths' => [], 'orders' => [], ], - [ - '$id' => ID::custom('_key_internal_path'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['databaseInternalId', 'collectionInternalId'], - 'lengths' => [], - 'orders' => [], - ], ], ], ]; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index cb6e843fd5..331a2668a3 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -1310,9 +1310,9 @@ class Deletes extends Action $dbForProject->deleteDocuments('transactionLogs', [ Query::equal('transactionInternalId', [$transactionInternalId]), ]); - Console::info("Transaction logs for {$transactionId} deleted."); + Console::info("Transaction logs for transaction {$transactionId} deleted."); } catch (Throwable $th) { - Console::error("Failed to delete transaction logs for {$transactionId}: " . $th->getMessage()); + Console::error("Failed to delete transaction logs for transaction {$transactionId}: " . $th->getMessage()); } } @@ -1323,7 +1323,6 @@ class Deletes extends Action try { $dbForProject->deleteDocuments('transactions', [ - Query::equal('status', ['pending']), Query::lessThan('expiresAt', DateTime::format(new \DateTime())), ], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) { $transactionInternalIds[] = $transaction->getSequence(); From 65ad2c9ff54cc97883ee0822e00e1585f7fad9b9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Sep 2025 01:45:16 +1200 Subject: [PATCH 094/145] Handle usage + realtime --- .../Http/Databases/Transactions/Update.php | 147 ++++++++++++++++-- .../Http/TablesDB/Transactions/Update.php | 5 + .../Legacy/Transactions/TransactionsTest.php | 2 +- .../Transactions/TransactionsTest.php | 2 +- 4 files changed, 139 insertions(+), 17 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index ed70afee35..e8d3e6a1e4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -3,6 +3,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Event\Delete; +use Appwrite\Event\Event; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -63,6 +65,11 @@ class Update extends Action ->inject('response') ->inject('dbForProject') ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->callback($this->action(...)); } @@ -73,6 +80,11 @@ class Update extends Action * @param UtopiaResponse $response * @param Database $dbForProject * @param Delete $queueForDeletes + * @param Event $queueForEvents + * @param StatsUsage $queueForStatsUsage + * @param Event $queueForRealtime + * @param Event $queueForFunctions + * @param Event $queueForWebhooks * @return void * @throws ConflictException * @throws Exception @@ -82,7 +94,7 @@ class Update extends Action * @throws Structure * @throws \Utopia\Exception */ - public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Delete $queueForDeletes): void + public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); @@ -106,13 +118,18 @@ class Update extends Action } if ($commit) { - $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, &$transaction) { + $operations = []; + + // Track metrics for usage stats + $totalOperations = 0; + $databaseOperations = []; + + $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', ])); - // Fetch operations ordered by sequence by default to - // replay operations in exact order they were created + // Fetch operations ordered by sequence by default to replay operations in exact order they were created $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), ]); @@ -130,6 +147,10 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; + // Track operations for stats + $totalOperations++; + $databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1; + if ($data instanceof Document) { $data = $data->getArrayCopy(); } @@ -196,13 +217,99 @@ class Update extends Action throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } }); + + $queueForStatsUsage + ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations); + + // Add per-database metrics + foreach ($databaseOperations as $sequence => $count) { + $queueForStatsUsage->addMetric( + str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES), + $count + ); + } + + // Trigger realtime events for each operation + foreach ($operations as $operation) { + $databaseInternalId = $operation['databaseInternalId']; + $collectionInternalId = $operation['collectionInternalId']; + $action = $operation['action']; + $documentId = $operation['documentId']; + $data = $operation['data']; + + if ($data instanceof Document) { + $data = $data->getArrayCopy(); + } + + $database = $dbForProject->findOne('databases', [ + Query::equal('$sequence', [$databaseInternalId]) + ]); + $collection = $dbForProject->findOne('database_' . $databaseInternalId, [ + Query::equal('$sequence', [$collectionInternalId]) + ]); + + $queueForEvents + ->setParam('databaseId', $database->getId()) + ->setContext('database', $database) + ->setParam('collectionId', $collection->getId()) + ->setParam('tableId', $collection->getId()) + ->setContext('collection', $collection); + + $eventAction = ''; + $documents = []; + + switch ($action) { + case 'create': + $eventAction = 'create'; + $documents[] = $documentId ?? $data['$id'] ?? null; + break; + case 'update': + case 'increment': + case 'decrement': + $eventAction = 'update'; + $documents[] = $documentId; + break; + case 'delete': + $eventAction = 'delete'; + $documents[] = $documentId; + break; + case 'upsert': + $eventAction = 'upsert'; + $documents[] = $documentId ?? $data['$id'] ?? null; + break; + case 'bulkCreate': + case 'bulkUpdate': + case 'bulkUpsert': + case 'bulkDelete': + break; + } + + // Trigger events for each document + foreach ($documents as $docId) { + if ($docId) { + $queueForEvents + ->setParam('documentId', $docId) + ->setParam('rowId', $docId) + ->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].' . $eventAction); + + $queueForRealtime->from($queueForEvents)->trigger(); + $queueForFunctions->from($queueForEvents)->trigger(); + $queueForWebhooks->from($queueForEvents)->trigger(); + + $queueForEvents->reset(); + $queueForRealtime->reset(); + $queueForFunctions->reset(); + $queueForWebhooks->reset(); + } + } + } } if ($rollback) { $transaction = $dbForProject->updateDocument( 'transactions', $transactionId, - new Document(['status' => 'rolledBack']) + new Document(['status' => 'failed']) ); $queueForDeletes @@ -226,7 +333,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { if ($documentId && !isset($data['$id'])) { $data['$id'] = $documentId; } @@ -250,7 +358,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -288,7 +397,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -318,7 +428,8 @@ class Update extends Action string $documentId, \DateTime $createdAt, array &$state - ): void { + ): void + { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -350,7 +461,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -387,7 +499,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -423,7 +536,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { $dbForProject->createDocuments( $collectionId, @@ -447,7 +561,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->updateDocuments( @@ -481,7 +596,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { // Run bulk upsert without timestamp wrapper, checking manually in callback $dbForProject->upsertDocuments( $collectionId, @@ -517,7 +633,8 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void { + ): void + { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->deleteDocuments( diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index b754ec97a1..d76de0766d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -53,6 +53,11 @@ class Update extends TransactionsUpdate ->inject('response') ->inject('dbForProject') ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->inject('queueForRealtime') + ->inject('queueForFunctions') + ->inject('queueForWebhooks') ->callback($this->action(...)); } } diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php index 34eb561d63..02e3082626 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php @@ -485,7 +485,7 @@ class TransactionsTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('rolledBack', $response['body']['status']); + $this->assertEquals('failed', $response['body']['status']); // Verify no documents were created $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index 9ec5994903..30bb4cb290 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -485,7 +485,7 @@ class TransactionsTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('rolledBack', $response['body']['status']); + $this->assertEquals('failed', $response['body']['status']); // Verify no rows were created $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ From 38dc92bfa792536d202eea56b314234840d16357 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Sep 2025 02:05:19 +1200 Subject: [PATCH 095/145] Delete with worker --- .../Http/Databases/Transactions/Delete.php | 15 +++++----- .../Http/Databases/Transactions/Update.php | 30 +++++++------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php index 43c0c5c4a8..da92ce1b4c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; +use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -9,7 +10,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; -use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -51,10 +51,11 @@ class Delete extends Action ->param('transactionId', '', new UID(), 'Transaction ID.') ->inject('response') ->inject('dbForProject') + ->inject('queueForDeletes') ->callback($this->action(...)); } - public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject): void + public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeleteEvent $queueForDeletes): void { $transaction = $dbForProject->getDocument('transactions', $transactionId); @@ -62,13 +63,11 @@ class Delete extends Action throw new Exception(Exception::TRANSACTION_NOT_FOUND); } - if (!$dbForProject->deleteDocument('transactions', $transactionId)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove transaction from DB'); - } + $dbForProject->deleteDocument('transactions', $transactionId); - $dbForProject->deleteDocuments('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - ]); + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($transaction); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index e8d3e6a1e4..54b339a46c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -333,8 +333,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { if ($documentId && !isset($data['$id'])) { $data['$id'] = $documentId; } @@ -358,8 +357,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -397,8 +395,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -428,8 +425,7 @@ class Update extends Action string $documentId, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -461,8 +457,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -499,8 +494,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { @@ -536,8 +530,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { $dbForProject->createDocuments( $collectionId, @@ -561,8 +554,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->updateDocuments( @@ -596,8 +588,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { // Run bulk upsert without timestamp wrapper, checking manually in callback $dbForProject->upsertDocuments( $collectionId, @@ -633,8 +624,7 @@ class Update extends Action array $data, \DateTime $createdAt, array &$state - ): void - { + ): void { $queries = Query::parseQueries($data['queries'] ?? []); $dbForProject->deleteDocuments( From 7120bce761aa3401d5065e7c3bf9eb6f2dc705a7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Sep 2025 21:47:40 +1200 Subject: [PATCH 096/145] Handle structure exception --- .../Http/Databases/Transactions/Operations/Create.php | 2 +- .../Databases/Http/Databases/Transactions/Update.php | 7 ++++++- .../Http/TablesDB/Transactions/Operations/Create.php | 2 +- .../Databases/Legacy/Transactions/TransactionsTest.php | 2 +- .../Databases/TablesDB/Transactions/TransactionsTest.php | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index a33af05e3d..d2e438706a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -36,7 +36,7 @@ class Create extends Action $this ->setHttpMethod(self::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/databases/transactions/:transactionId/operations') - ->desc('Add operations to transaction') + ->desc('Create operations scoped to a transaction') ->groups(['api', 'database', 'transactions']) ->label('scope', 'transactions.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 54b339a46c..2c555d433c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -17,7 +17,7 @@ use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; -use Utopia\Database\Exception\Structure; +use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; use Utopia\Database\Validator\UID; @@ -210,6 +210,11 @@ class Update extends Action 'status' => 'failed', ])); throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); + } catch (StructureException $e) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (TransactionException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php index 6b2ee2ce4c..2280a6f7e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -30,7 +30,7 @@ class Create extends OperationsCreate $this ->setHttpMethod(self::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/tablesdb/transactions/:transactionId/operations') - ->desc('Add operations to transaction') + ->desc('Create operations scoped to a transaction') ->groups(['api', 'database', 'transactions']) ->label('scope', 'transactions.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php index 02e3082626..9d8cb7961e 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php @@ -96,7 +96,7 @@ class TransactionsTest extends Scope /** * Test adding operations to a transaction */ - public function testAddOperations(): void + public function testCreateOperations(): void { // Create database first $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index 30bb4cb290..68f0afb835 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -96,7 +96,7 @@ class TransactionsTest extends Scope /** * Test adding operations to a transaction */ - public function testAddOperations(): void + public function testCreateOperations(): void { // Create database first $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ From 4f916fae220e30fe3bbe65fbfd35e7fa2d762847 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 13 Sep 2025 00:04:37 +1200 Subject: [PATCH 097/145] Handle limit exception --- .../Databases/Http/Databases/Transactions/Update.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 2c555d433c..4509114bfc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -16,6 +16,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Transaction as TransactionException; @@ -215,6 +216,11 @@ class Update extends Action 'status' => 'failed', ])); throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } catch (LimitException $e) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); } catch (TransactionException $e) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', From 304ebf12b91bdaf38e61054ea779b0aa666d2bea Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 13 Sep 2025 00:04:49 +1200 Subject: [PATCH 098/145] Add more tests --- .../Legacy/Transactions/TransactionsTest.php | 596 ++++++++++++++++++ .../Transactions/TransactionsTest.php | 596 ++++++++++++++++++ 2 files changed, 1192 insertions(+) diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php index 9d8cb7961e..d712c2ca6c 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php @@ -3635,4 +3635,600 @@ class TransactionsTest extends Scope $this->assertEquals('active', $response['body']['status']); } } + + /** + * Test increment and decrement operations in transaction + */ + public function testIncrementDecrementOperations(): 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' => 'IncrementDecrementTestDB' + ]); + + $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' => 'CounterCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add integer attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'counter', + 'required' => false, + 'default' => 0, + ]); + + $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' => 'score', + 'required' => false, + 'default' => 100, + ]); + + sleep(2); + + // Create initial document + $doc = $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' => 'counter_doc', + 'data' => [ + 'counter' => 10, + 'score' => 50 + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add increment and decrement operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'increment', + 'documentId' => 'counter_doc', + 'data' => [ + 'attribute' => 'counter', + 'value' => 5, + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'decrement', + 'documentId' => 'counter_doc', + 'data' => [ + 'attribute' => 'score', + 'value' => 20, + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'increment', + 'documentId' => 'counter_doc', + 'data' => [ + 'attribute' => 'counter', + 'value' => 3, + 'max' => 20 + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'decrement', + 'documentId' => 'counter_doc', + 'data' => [ + 'attribute' => 'score', + 'value' => 30, + 'min' => 0 + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify final values + $doc = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/counter_doc", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $doc['headers']['status-code']); + // counter: 10 + 5 + 3 = 18 (capped at 20 max) + $this->assertEquals(18, $doc['body']['counter']); + // score: 50 - 20 - 100 = -70, but min is 0 + $this->assertEquals(0, $doc['body']['score']); + } + + /** + * Test bulk update operations in transaction + */ + public function testBulkUpdateOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpdateTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'BulkUpdateCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 50, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + // Create initial documents + for ($i = 1; $i <= 5; $i++) { + $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' => "doc_{$i}", + 'data' => [ + 'status' => 'pending', + 'category' => $i % 2 === 0 ? 'even' : 'odd' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk update operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [ + Query::equal('category', ['even'])->toString() + ], + 'data' => [ + 'status' => 'approved' + ] + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [ + Query::equal('category', ['odd'])->toString() + ], + 'data' => [ + 'status' => 'rejected' + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify updates + $docs = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + foreach ($docs['body']['documents'] as $doc) { + if ($doc['category'] === 'even') { + $this->assertEquals('approved', $doc['status']); + } else { + $this->assertEquals('rejected', $doc['status']); + } + } + } + + /** + * Test bulk upsert operations in transaction + */ + public function testBulkUpsertOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkUpsertTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'BulkUpsertCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 100, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'value', + 'required' => false, + ]); + + sleep(2); + + // Create some 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' => 'existing_1', + 'data' => [ + 'name' => 'Existing Document 1', + 'value' => 10 + ] + ]); + + $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' => 'existing_2', + 'data' => [ + 'name' => 'Existing Document 2', + 'value' => 20 + ] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk upsert operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkUpsert', + 'data' => [ + [ + '$id' => 'existing_1', + 'name' => 'Updated Document 1', + 'value' => 100 + ], + [ + '$id' => 'new_1', + 'name' => 'New Document 1', + 'value' => 30 + ], + [ + '$id' => 'new_2', + 'name' => 'New Document 2', + 'value' => 40 + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify results + $docs = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(4, $docs['body']['total']); + + $docMap = []; + foreach ($docs['body']['documents'] as $doc) { + $docMap[$doc['$id']] = $doc; + } + + // Verify updated document + $this->assertEquals('Updated Document 1', $docMap['existing_1']['name']); + $this->assertEquals(100, $docMap['existing_1']['value']); + + // Verify unchanged document + $this->assertEquals('Existing Document 2', $docMap['existing_2']['name']); + $this->assertEquals(20, $docMap['existing_2']['value']); + + // Verify new documents + $this->assertEquals('New Document 1', $docMap['new_1']['name']); + $this->assertEquals(30, $docMap['new_1']['value']); + $this->assertEquals('New Document 2', $docMap['new_2']['name']); + $this->assertEquals(40, $docMap['new_2']['value']); + } + + /** + * Test bulk delete operations in transaction + */ + public function testBulkDeleteOperations(): void + { + // Create database and collection + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'BulkDeleteTestDB' + ]); + + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'BulkDeleteCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $collectionId = $collection['body']['$id']; + + // Add attributes + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'type', + 'size' => 50, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'priority', + 'required' => false, + ]); + + sleep(2); + + // Create initial documents + for ($i = 1; $i <= 10; $i++) { + $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' => "doc_{$i}", + 'data' => [ + 'type' => $i <= 5 ? 'temp' : 'permanent', + 'priority' => $i + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk delete operations + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [ + Query::equal('type', ['temp'])->toString() + ] + ] + ], + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [ + Query::greaterThan('priority', 8)->toString() + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify deletions + $docs = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have deleted docs 1-5 (temp) and docs 9-10 (priority > 8) + // Remaining should be docs 6-8 + $this->assertEquals(3, $docs['body']['total']); + + $remainingIds = array_map(fn($doc) => $doc['$id'], $docs['body']['documents']); + sort($remainingIds); + $this->assertEquals(['doc_6', 'doc_7', 'doc_8'], $remainingIds); + } } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index 68f0afb835..4466d53e30 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -3635,4 +3635,600 @@ class TransactionsTest extends Scope $this->assertEquals('active', $response['body']['status']); } } + + /** + * Test increment and decrement operations in transaction + */ + public function testIncrementDecrementOperations(): 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' => 'IncrementDecrementTestDB' + ]); + + $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' => 'CounterTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add integer columns + $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' => 'counter', + 'required' => false, + 'default' => 0, + ]); + + $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' => 'score', + 'required' => false, + 'default' => 100, + ]); + + sleep(2); + + // Create initial row + $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' => 'counter_row', + 'data' => [ + 'counter' => 10, + 'score' => 50 + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add increment and decrement operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'increment', + 'rowId' => 'counter_row', + 'data' => [ + 'column' => 'counter', + 'value' => 5, + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'decrement', + 'rowId' => 'counter_row', + 'data' => [ + 'column' => 'score', + 'value' => 20, + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'increment', + 'rowId' => 'counter_row', + 'data' => [ + 'column' => 'counter', + 'value' => 3, + 'max' => 20 + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'decrement', + 'rowId' => 'counter_row', + 'data' => [ + 'column' => 'score', + 'value' => 30, + 'min' => 0 + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify final values + $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/counter_row", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + // counter: 10 + 5 + 3 = 18 (capped at 20 max) + $this->assertEquals(18, $row['body']['counter']); + // score: 50 - 20 - 100 = -70, but min is 0 + $this->assertEquals(0, $row['body']['score']); + } + + /** + * Test bulk update operations in transaction + */ + public function testBulkUpdateOperations(): 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' => 'BulkUpdateTestDB' + ]); + + $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' => 'BulkUpdateTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 50, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'category', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + // Create initial rows + for ($i = 1; $i <= 5; $i++) { + $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' => "row_{$i}", + 'data' => [ + 'status' => 'pending', + 'category' => $i % 2 === 0 ? 'even' : 'odd' + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk update operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [ + Query::equal('category', ['even'])->toString() + ], + 'data' => [ + 'status' => 'approved' + ] + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [ + Query::equal('category', ['odd'])->toString() + ], + 'data' => [ + 'status' => 'rejected' + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify updates + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + foreach ($rows['body']['rows'] as $row) { + if ($row['category'] === 'even') { + $this->assertEquals('approved', $row['status']); + } else { + $this->assertEquals('rejected', $row['status']); + } + } + } + + /** + * Test bulk upsert operations in transaction + */ + public function testBulkUpsertOperations(): 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' => 'BulkUpsertTestDB' + ]); + + $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' => 'BulkUpsertTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 100, + 'required' => false, + ]); + + $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' => 'value', + 'required' => false, + ]); + + sleep(2); + + // Create some 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' => 'existing_1', + 'data' => [ + 'name' => 'Existing Row 1', + 'value' => 10 + ] + ]); + + $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' => 'existing_2', + 'data' => [ + 'name' => 'Existing Row 2', + 'value' => 20 + ] + ]); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk upsert operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkUpsert', + 'data' => [ + [ + '$id' => 'existing_1', + 'name' => 'Updated Row 1', + 'value' => 100 + ], + [ + '$id' => 'new_1', + 'name' => 'New Row 1', + 'value' => 30 + ], + [ + '$id' => 'new_2', + 'name' => 'New Row 2', + 'value' => 40 + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify results + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(4, $rows['body']['total']); + + $rowMap = []; + foreach ($rows['body']['rows'] as $row) { + $rowMap[$row['$id']] = $row; + } + + // Verify updated row + $this->assertEquals('Updated Row 1', $rowMap['existing_1']['name']); + $this->assertEquals(100, $rowMap['existing_1']['value']); + + // Verify unchanged row + $this->assertEquals('Existing Row 2', $rowMap['existing_2']['name']); + $this->assertEquals(20, $rowMap['existing_2']['value']); + + // Verify new rows + $this->assertEquals('New Row 1', $rowMap['new_1']['name']); + $this->assertEquals(30, $rowMap['new_1']['value']); + $this->assertEquals('New Row 2', $rowMap['new_2']['name']); + $this->assertEquals(40, $rowMap['new_2']['value']); + } + + /** + * Test bulk delete operations in transaction + */ + public function testBulkDeleteOperations(): 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' => 'BulkDeleteTestDB' + ]); + + $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' => 'BulkDeleteTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'type', + 'size' => 50, + 'required' => false, + ]); + + $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' => 'priority', + 'required' => false, + ]); + + sleep(2); + + // Create initial rows + for ($i = 1; $i <= 10; $i++) { + $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' => "row_{$i}", + 'data' => [ + 'type' => $i <= 5 ? 'temp' : 'permanent', + 'priority' => $i + ] + ]); + } + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $transactionId = $transaction['body']['$id']; + + // Add bulk delete operations + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [ + Query::equal('type', ['temp'])->toString() + ] + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [ + Query::greaterThan('priority', 8)->toString() + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify deletions + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Should have deleted rows 1-5 (temp) and rows 9-10 (priority > 8) + // Remaining should be rows 6-8 + $this->assertEquals(3, $rows['body']['total']); + + $remainingIds = array_map(fn($row) => $row['$id'], $rows['body']['rows']); + sort($remainingIds); + $this->assertEquals(['row_6', 'row_7', 'row_8'], $remainingIds); + } } From e5a95ed990ec8160a49a9da5292f584bae6dc822 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 13 Sep 2025 00:31:35 +1200 Subject: [PATCH 099/145] Update DB --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index ba8b58fcea..c532f24ef2 100644 --- a/composer.lock +++ b/composer.lock @@ -3638,16 +3638,16 @@ }, { "name": "utopia-php/database", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "44af82dcb44cdaa4b2d7f30528903f186a972ee0" + "reference": "e68f2e358bdfbc682aaad4956d325535664ec7d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/44af82dcb44cdaa4b2d7f30528903f186a972ee0", - "reference": "44af82dcb44cdaa4b2d7f30528903f186a972ee0", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e68f2e358bdfbc682aaad4956d325535664ec7d5", + "reference": "e68f2e358bdfbc682aaad4956d325535664ec7d5", "shasum": "" }, "require": { @@ -3688,9 +3688,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/2.0.1" + "source": "https://github.com/utopia-php/database/tree/2.0.2" }, - "time": "2025-09-11T08:33:25+00:00" + "time": "2025-09-12T12:29:38+00:00" }, { "name": "utopia-php/detector", From 5c09e8ae00f19b203cd9ce907066fad14b8581ea Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 13 Sep 2025 00:35:03 +1200 Subject: [PATCH 100/145] Lint --- .../Services/Databases/Legacy/Transactions/TransactionsTest.php | 2 +- .../Databases/TablesDB/Transactions/TransactionsTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php index d712c2ca6c..a76d909b5e 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php @@ -4227,7 +4227,7 @@ class TransactionsTest extends Scope // Remaining should be docs 6-8 $this->assertEquals(3, $docs['body']['total']); - $remainingIds = array_map(fn($doc) => $doc['$id'], $docs['body']['documents']); + $remainingIds = array_map(fn ($doc) => $doc['$id'], $docs['body']['documents']); sort($remainingIds); $this->assertEquals(['doc_6', 'doc_7', 'doc_8'], $remainingIds); } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php index 4466d53e30..910b675afc 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsTest.php @@ -4227,7 +4227,7 @@ class TransactionsTest extends Scope // Remaining should be rows 6-8 $this->assertEquals(3, $rows['body']['total']); - $remainingIds = array_map(fn($row) => $row['$id'], $rows['body']['rows']); + $remainingIds = array_map(fn ($row) => $row['$id'], $rows['body']['rows']); sort($remainingIds); $this->assertEquals(['row_6', 'row_7', 'row_8'], $remainingIds); } From 0f9c2001f7a320e20e606db737f9496c4daa6c06 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 13 Sep 2025 02:42:42 +1200 Subject: [PATCH 101/145] Demo SDK --- app/config/platforms.php | 8 +++---- app/config/specs/open-api3-1.8.x-client.json | 4 ++-- app/config/specs/open-api3-1.8.x-console.json | 4 ++-- app/config/specs/open-api3-1.8.x-server.json | 4 ++-- app/config/specs/open-api3-latest-client.json | 8 +++---- .../specs/open-api3-latest-console.json | 8 +++---- app/config/specs/open-api3-latest-server.json | 8 +++---- app/config/specs/swagger2-1.8.x-client.json | 4 ++-- app/config/specs/swagger2-1.8.x-console.json | 4 ++-- app/config/specs/swagger2-1.8.x-server.json | 4 ++-- app/config/specs/swagger2-latest-client.json | 8 +++---- app/config/specs/swagger2-latest-console.json | 8 +++---- app/config/specs/swagger2-latest-server.json | 8 +++---- .../examples/databases/create-document.md | 3 ++- .../examples/databases/create-operations.md | 24 +++++++++++++++++++ .../examples/databases/create-transaction.md | 13 ++++++++++ .../databases/decrement-document-attribute.md | 3 ++- .../examples/databases/delete-document.md | 3 ++- .../examples/databases/delete-transaction.md | 13 ++++++++++ .../examples/databases/get-document.md | 3 ++- .../examples/databases/get-transaction.md | 13 ++++++++++ .../databases/increment-document-attribute.md | 3 ++- .../examples/databases/list-documents.md | 3 ++- .../examples/databases/list-transactions.md | 13 ++++++++++ .../examples/databases/update-document.md | 3 ++- .../examples/databases/update-transaction.md | 15 ++++++++++++ .../examples/databases/upsert-document.md | 3 ++- .../examples/tablesdb/create-operations.md | 24 +++++++++++++++++++ .../examples/tablesdb/create-row.md | 3 ++- .../examples/tablesdb/create-transaction.md | 13 ++++++++++ .../examples/tablesdb/decrement-row-column.md | 3 ++- .../examples/tablesdb/delete-row.md | 3 ++- .../examples/tablesdb/delete-transaction.md | 13 ++++++++++ .../client-web/examples/tablesdb/get-row.md | 3 ++- .../examples/tablesdb/get-transaction.md | 13 ++++++++++ .../examples/tablesdb/increment-row-column.md | 3 ++- .../client-web/examples/tablesdb/list-rows.md | 3 ++- .../examples/tablesdb/list-transactions.md | 13 ++++++++++ .../examples/tablesdb/update-row.md | 3 ++- .../examples/tablesdb/update-transaction.md | 15 ++++++++++++ .../examples/tablesdb/upsert-row.md | 3 ++- .../examples/databases/create-document.md | 3 ++- .../examples/databases/create-documents.md | 3 ++- .../examples/databases/create-operations.md | 23 ++++++++++++++++++ .../examples/databases/create-transaction.md | 12 ++++++++++ .../databases/decrement-document-attribute.md | 3 ++- .../examples/databases/delete-document.md | 3 ++- .../examples/databases/delete-documents.md | 3 ++- .../examples/databases/delete-transaction.md | 12 ++++++++++ .../examples/databases/get-document.md | 3 ++- .../examples/databases/get-transaction.md | 12 ++++++++++ .../databases/increment-document-attribute.md | 3 ++- .../examples/databases/list-documents.md | 3 ++- .../examples/databases/list-transactions.md | 12 ++++++++++ .../examples/databases/update-document.md | 3 ++- .../examples/databases/update-documents.md | 3 ++- .../examples/databases/update-transaction.md | 14 +++++++++++ .../examples/databases/upsert-document.md | 3 ++- .../examples/databases/upsert-documents.md | 3 ++- .../examples/messaging/create-push.md | 2 +- .../examples/messaging/update-push.md | 2 +- .../examples/tablesdb/create-operations.md | 23 ++++++++++++++++++ .../examples/tablesdb/create-row.md | 3 ++- .../examples/tablesdb/create-rows.md | 3 ++- .../examples/tablesdb/create-transaction.md | 12 ++++++++++ .../examples/tablesdb/decrement-row-column.md | 3 ++- .../examples/tablesdb/delete-row.md | 3 ++- .../examples/tablesdb/delete-rows.md | 3 ++- .../examples/tablesdb/delete-transaction.md | 12 ++++++++++ .../examples/tablesdb/get-row.md | 3 ++- .../examples/tablesdb/get-transaction.md | 12 ++++++++++ .../examples/tablesdb/increment-row-column.md | 3 ++- .../examples/tablesdb/list-rows.md | 3 ++- .../examples/tablesdb/list-transactions.md | 12 ++++++++++ .../examples/tablesdb/update-row.md | 3 ++- .../examples/tablesdb/update-rows.md | 3 ++- .../examples/tablesdb/update-transaction.md | 14 +++++++++++ .../examples/tablesdb/upsert-row.md | 3 ++- .../examples/tablesdb/upsert-rows.md | 3 ++- 79 files changed, 474 insertions(+), 82 deletions(-) create mode 100644 docs/examples/1.8.x/client-web/examples/databases/create-operations.md create mode 100644 docs/examples/1.8.x/client-web/examples/databases/create-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/databases/delete-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/databases/get-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/databases/list-transactions.md create mode 100644 docs/examples/1.8.x/client-web/examples/databases/update-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/create-operations.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/create-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/delete-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/get-transaction.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/list-transactions.md create mode 100644 docs/examples/1.8.x/client-web/examples/tablesdb/update-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/create-operations.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/create-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/delete-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/get-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/list-transactions.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/databases/update-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-operations.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-transaction.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-transactions.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-transaction.md diff --git a/app/config/platforms.php b/app/config/platforms.php index 7623bb896e..5cc62d6e23 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -11,7 +11,7 @@ return [ [ 'key' => 'web', 'name' => 'Web', - 'version' => '20.0.0', + 'version' => '20.1.0-rc.1', 'url' => 'https://github.com/appwrite/sdk-for-web', 'package' => 'https://www.npmjs.com/package/appwrite', 'enabled' => true, @@ -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' => [ [ @@ -262,7 +262,7 @@ return [ [ 'key' => 'nodejs', 'name' => 'Node.js', - 'version' => '19.0.0', + 'version' => '19.1.0-rc.1', 'url' => 'https://github.com/appwrite/sdk-for-node', 'package' => 'https://www.npmjs.com/package/node-appwrite', 'enabled' => true, @@ -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'), ], [ diff --git a/app/config/specs/open-api3-1.8.x-client.json b/app/config/specs/open-api3-1.8.x-client.json index e94ac895b6..6d5acf8c86 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -5143,7 +5143,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -8285,7 +8285,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index 1ff651107b..797a7e72e4 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -5542,7 +5542,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -33677,7 +33677,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index 987ae7fece..8e8c33ace4 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -5090,7 +5090,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -24203,7 +24203,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index a24d36fe76..6d5acf8c86 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -5143,7 +5143,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -5212,7 +5212,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -8285,7 +8285,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" @@ -8354,7 +8354,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 3316be2f8b..797a7e72e4 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -5542,7 +5542,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -5611,7 +5611,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -33677,7 +33677,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" @@ -33746,7 +33746,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index a2d91def99..8e8c33ace4 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -5090,7 +5090,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "tags": [ "databases" @@ -5161,7 +5161,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -24203,7 +24203,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "tags": [ "tablesDB" @@ -24274,7 +24274,7 @@ "operations": { "type": "array", "description": "Array of staged operations.", - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index 7297412924..e87ce88a6c 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -5282,7 +5282,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -8359,7 +8359,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 5e8aad66e4..16744679ec 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -5701,7 +5701,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -33793,7 +33793,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index 9dd44734bb..d3f7129c95 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -5237,7 +5237,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -24375,7 +24375,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 4d0e75c6fa..e87ce88a6c 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -5282,7 +5282,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -5350,7 +5350,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -8359,7 +8359,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" @@ -8427,7 +8427,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index c39987cbaf..16744679ec 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5701,7 +5701,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -5769,7 +5769,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -33793,7 +33793,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" @@ -33861,7 +33861,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 7e0785fc0a..d3f7129c95 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -5237,7 +5237,7 @@ }, "\/databases\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "databasesCreateOperations", "consumes": [ "application\/json" @@ -5307,7 +5307,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"collectionId\": \"\",\n \"documentId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"collectionId\": \"\",\n\t \"documentId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } @@ -24375,7 +24375,7 @@ }, "\/tablesdb\/transactions\/{transactionId}\/operations": { "post": { - "summary": "Add operations to transaction", + "summary": "Create operations scoped to a transaction", "operationId": "tablesDBCreateOperations", "consumes": [ "application\/json" @@ -24445,7 +24445,7 @@ "type": "array", "description": "Array of staged operations.", "default": [], - "x-example": "[\n {\n \"action\": \"create\",\n \"databaseId\": \"\",\n \"tableId\": \"\",\n \"rowId\": \"\",\n \"data\": {\n \"name\": \"Walter O'Brien\"\n }\n }\n]", + "x-example": "[\n\t {\n\t \"action\": \"create\",\n\t \"databaseId\": \"\",\n\t \"tableId\": \"\",\n\t \"rowId\": \"\",\n\t \"data\": {\n\t \"name\": \"Walter O'Brien\"\n\t }\n\t }\n\t]", "items": { "type": "object" } diff --git a/docs/examples/1.8.x/client-web/examples/databases/create-document.md b/docs/examples/1.8.x/client-web/examples/databases/create-document.md index 08606c9b4f..8417575ebf 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/create-document.md +++ b/docs/examples/1.8.x/client-web/examples/databases/create-document.md @@ -17,7 +17,8 @@ const result = await databases.createDocument({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/create-operations.md b/docs/examples/1.8.x/client-web/examples/databases/create-operations.md new file mode 100644 index 0000000000..2ebc085d44 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/create-operations.md @@ -0,0 +1,24 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.createOperations({ + transactionId: '', + operations: [ + { + "action": "create", + "databaseId": "", + "collectionId": "", + "documentId": "", + "data": { + "name": "Walter O'Brien" + } + } + ] // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/create-transaction.md b/docs/examples/1.8.x/client-web/examples/databases/create-transaction.md new file mode 100644 index 0000000000..5371412cc9 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/create-transaction.md @@ -0,0 +1,13 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.createTransaction({ + ttl: 60 // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/decrement-document-attribute.md b/docs/examples/1.8.x/client-web/examples/databases/decrement-document-attribute.md index 98629c4e6c..f8e0ae9829 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/decrement-document-attribute.md +++ b/docs/examples/1.8.x/client-web/examples/databases/decrement-document-attribute.md @@ -12,7 +12,8 @@ const result = await databases.decrementDocumentAttribute({ documentId: '', attribute: '', value: null, // optional - min: null // optional + min: null, // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/delete-document.md b/docs/examples/1.8.x/client-web/examples/databases/delete-document.md index 4192085653..4d10afdac5 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/delete-document.md +++ b/docs/examples/1.8.x/client-web/examples/databases/delete-document.md @@ -9,7 +9,8 @@ const databases = new Databases(client); const result = await databases.deleteDocument({ databaseId: '', collectionId: '', - documentId: '' + documentId: '', + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/delete-transaction.md b/docs/examples/1.8.x/client-web/examples/databases/delete-transaction.md new file mode 100644 index 0000000000..afe55c547a --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/delete-transaction.md @@ -0,0 +1,13 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.deleteTransaction({ + transactionId: '' +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/get-document.md b/docs/examples/1.8.x/client-web/examples/databases/get-document.md index b3a7558907..5a44aeb73e 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/get-document.md +++ b/docs/examples/1.8.x/client-web/examples/databases/get-document.md @@ -10,7 +10,8 @@ const result = await databases.getDocument({ databaseId: '', collectionId: '', documentId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/get-transaction.md b/docs/examples/1.8.x/client-web/examples/databases/get-transaction.md new file mode 100644 index 0000000000..cc51199a76 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/get-transaction.md @@ -0,0 +1,13 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.getTransaction({ + transactionId: '' +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/increment-document-attribute.md b/docs/examples/1.8.x/client-web/examples/databases/increment-document-attribute.md index 8adb5d8091..eaf718e98d 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/increment-document-attribute.md +++ b/docs/examples/1.8.x/client-web/examples/databases/increment-document-attribute.md @@ -12,7 +12,8 @@ const result = await databases.incrementDocumentAttribute({ documentId: '', attribute: '', value: null, // optional - max: null // optional + max: null, // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/list-documents.md b/docs/examples/1.8.x/client-web/examples/databases/list-documents.md index fb1d508fed..ece656a644 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/list-documents.md +++ b/docs/examples/1.8.x/client-web/examples/databases/list-documents.md @@ -9,7 +9,8 @@ const databases = new Databases(client); const result = await databases.listDocuments({ databaseId: '', collectionId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/list-transactions.md b/docs/examples/1.8.x/client-web/examples/databases/list-transactions.md new file mode 100644 index 0000000000..f2ce1f7536 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/list-transactions.md @@ -0,0 +1,13 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.listTransactions({ + queries: [] // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/update-document.md b/docs/examples/1.8.x/client-web/examples/databases/update-document.md index bf3554812d..33a6d73a12 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/update-document.md +++ b/docs/examples/1.8.x/client-web/examples/databases/update-document.md @@ -11,7 +11,8 @@ const result = await databases.updateDocument({ collectionId: '', documentId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/update-transaction.md b/docs/examples/1.8.x/client-web/examples/databases/update-transaction.md new file mode 100644 index 0000000000..9274b0f9bf --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/databases/update-transaction.md @@ -0,0 +1,15 @@ +import { Client, Databases } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const databases = new Databases(client); + +const result = await databases.updateTransaction({ + transactionId: '', + commit: false, // optional + rollback: false // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/databases/upsert-document.md b/docs/examples/1.8.x/client-web/examples/databases/upsert-document.md index c56bc5534d..e14ad5fc6b 100644 --- a/docs/examples/1.8.x/client-web/examples/databases/upsert-document.md +++ b/docs/examples/1.8.x/client-web/examples/databases/upsert-document.md @@ -11,7 +11,8 @@ const result = await databases.upsertDocument({ collectionId: '', documentId: '', data: {}, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/create-operations.md b/docs/examples/1.8.x/client-web/examples/tablesdb/create-operations.md new file mode 100644 index 0000000000..c25b051430 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/create-operations.md @@ -0,0 +1,24 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.createOperations({ + transactionId: '', + operations: [ + { + "action": "create", + "databaseId": "", + "tableId": "", + "rowId": "", + "data": { + "name": "Walter O'Brien" + } + } + ] // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/create-row.md b/docs/examples/1.8.x/client-web/examples/tablesdb/create-row.md index 1dd1fe4241..3bbcf89d4f 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/create-row.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/create-row.md @@ -17,7 +17,8 @@ const result = await tablesDB.createRow({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/create-transaction.md b/docs/examples/1.8.x/client-web/examples/tablesdb/create-transaction.md new file mode 100644 index 0000000000..17787dc9a3 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/create-transaction.md @@ -0,0 +1,13 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.createTransaction({ + ttl: 60 // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/decrement-row-column.md b/docs/examples/1.8.x/client-web/examples/tablesdb/decrement-row-column.md index 59f66d973f..d6c64645f3 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/decrement-row-column.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/decrement-row-column.md @@ -12,7 +12,8 @@ const result = await tablesDB.decrementRowColumn({ rowId: '', column: '', value: null, // optional - min: null // optional + min: null, // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/delete-row.md b/docs/examples/1.8.x/client-web/examples/tablesdb/delete-row.md index 637114d413..54c005c702 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/delete-row.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/delete-row.md @@ -9,7 +9,8 @@ const tablesDB = new TablesDB(client); const result = await tablesDB.deleteRow({ databaseId: '', tableId: '', - rowId: '' + rowId: '', + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/delete-transaction.md b/docs/examples/1.8.x/client-web/examples/tablesdb/delete-transaction.md new file mode 100644 index 0000000000..2ff1198225 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/delete-transaction.md @@ -0,0 +1,13 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.deleteTransaction({ + transactionId: '' +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/get-row.md b/docs/examples/1.8.x/client-web/examples/tablesdb/get-row.md index 4e436432b7..b345d145aa 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/get-row.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/get-row.md @@ -10,7 +10,8 @@ const result = await tablesDB.getRow({ databaseId: '', tableId: '', rowId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/get-transaction.md b/docs/examples/1.8.x/client-web/examples/tablesdb/get-transaction.md new file mode 100644 index 0000000000..8e2f24cd4c --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/get-transaction.md @@ -0,0 +1,13 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.getTransaction({ + transactionId: '' +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/increment-row-column.md b/docs/examples/1.8.x/client-web/examples/tablesdb/increment-row-column.md index a7f3a8c312..5baca80c35 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/increment-row-column.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/increment-row-column.md @@ -12,7 +12,8 @@ const result = await tablesDB.incrementRowColumn({ rowId: '', column: '', value: null, // optional - max: null // optional + max: null, // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/list-rows.md b/docs/examples/1.8.x/client-web/examples/tablesdb/list-rows.md index 63149aaba4..c0efd8486c 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/list-rows.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/list-rows.md @@ -9,7 +9,8 @@ const tablesDB = new TablesDB(client); const result = await tablesDB.listRows({ databaseId: '', tableId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/list-transactions.md b/docs/examples/1.8.x/client-web/examples/tablesdb/list-transactions.md new file mode 100644 index 0000000000..fbf0908a81 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/list-transactions.md @@ -0,0 +1,13 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.listTransactions({ + queries: [] // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/update-row.md b/docs/examples/1.8.x/client-web/examples/tablesdb/update-row.md index 1dba006762..ecbcd4fc7a 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/update-row.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/update-row.md @@ -11,7 +11,8 @@ const result = await tablesDB.updateRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/update-transaction.md b/docs/examples/1.8.x/client-web/examples/tablesdb/update-transaction.md new file mode 100644 index 0000000000..2d987e4235 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/update-transaction.md @@ -0,0 +1,15 @@ +import { Client, TablesDB } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.updateTransaction({ + transactionId: '', + commit: false, // optional + rollback: false // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-web/examples/tablesdb/upsert-row.md b/docs/examples/1.8.x/client-web/examples/tablesdb/upsert-row.md index 1add1c45b9..ddac9ff327 100644 --- a/docs/examples/1.8.x/client-web/examples/tablesdb/upsert-row.md +++ b/docs/examples/1.8.x/client-web/examples/tablesdb/upsert-row.md @@ -11,7 +11,8 @@ const result = await tablesDB.upsertRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); console.log(result); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md index 175e06301e..6fe77c42be 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md @@ -18,5 +18,6 @@ const result = await databases.createDocument({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-documents.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-documents.md index 73d08aa21e..8815d8d90f 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/create-documents.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-documents.md @@ -10,5 +10,6 @@ const databases = new sdk.Databases(client); const result = await databases.createDocuments({ databaseId: '', collectionId: '', - documents: [] + documents: [], + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-operations.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-operations.md new file mode 100644 index 0000000000..da8452e3db --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-operations.md @@ -0,0 +1,23 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.createOperations({ + transactionId: '', + operations: [ + { + "action": "create", + "databaseId": "", + "collectionId": "", + "documentId": "", + "data": { + "name": "Walter O'Brien" + } + } + ] // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-transaction.md new file mode 100644 index 0000000000..f3da2919f8 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.createTransaction({ + ttl: 60 // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/decrement-document-attribute.md b/docs/examples/1.8.x/server-nodejs/examples/databases/decrement-document-attribute.md index 87e4d7d25c..c01b250b08 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/decrement-document-attribute.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/decrement-document-attribute.md @@ -13,5 +13,6 @@ const result = await databases.decrementDocumentAttribute({ documentId: '', attribute: '', value: null, // optional - min: null // optional + min: null, // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/delete-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-document.md index 429554b74b..bfc19777f9 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/delete-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-document.md @@ -10,5 +10,6 @@ const databases = new sdk.Databases(client); const result = await databases.deleteDocument({ databaseId: '', collectionId: '', - documentId: '' + documentId: '', + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/delete-documents.md b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-documents.md index 0965d8ddaf..9440d20999 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/delete-documents.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-documents.md @@ -10,5 +10,6 @@ const databases = new sdk.Databases(client); const result = await databases.deleteDocuments({ databaseId: '', collectionId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/delete-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-transaction.md new file mode 100644 index 0000000000..53d676e74c --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/delete-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.deleteTransaction({ + transactionId: '' +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/get-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/get-document.md index 7cc71cd0c3..7abea4e44e 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/get-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/get-document.md @@ -11,5 +11,6 @@ const result = await databases.getDocument({ databaseId: '', collectionId: '', documentId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/get-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/databases/get-transaction.md new file mode 100644 index 0000000000..9b7297c7e7 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/get-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.getTransaction({ + transactionId: '' +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/increment-document-attribute.md b/docs/examples/1.8.x/server-nodejs/examples/databases/increment-document-attribute.md index 2b47f5a75d..843d163bca 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/increment-document-attribute.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/increment-document-attribute.md @@ -13,5 +13,6 @@ const result = await databases.incrementDocumentAttribute({ documentId: '', attribute: '', value: null, // optional - max: null // optional + max: null, // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/list-documents.md b/docs/examples/1.8.x/server-nodejs/examples/databases/list-documents.md index 148bf83c8f..7405f3e28d 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/list-documents.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/list-documents.md @@ -10,5 +10,6 @@ const databases = new sdk.Databases(client); const result = await databases.listDocuments({ databaseId: '', collectionId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/list-transactions.md b/docs/examples/1.8.x/server-nodejs/examples/databases/list-transactions.md new file mode 100644 index 0000000000..9a36eb0f93 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/list-transactions.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.listTransactions({ + queries: [] // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md index 8a9a6856b4..3e953760a1 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md @@ -12,5 +12,6 @@ const result = await databases.updateDocument({ collectionId: '', documentId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/update-documents.md b/docs/examples/1.8.x/server-nodejs/examples/databases/update-documents.md index 2cfb8c7800..dc79e433aa 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/update-documents.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/update-documents.md @@ -11,5 +11,6 @@ const result = await databases.updateDocuments({ databaseId: '', collectionId: '', data: {}, // optional - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/update-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/databases/update-transaction.md new file mode 100644 index 0000000000..57654495ba --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/update-transaction.md @@ -0,0 +1,14 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const databases = new sdk.Databases(client); + +const result = await databases.updateTransaction({ + transactionId: '', + commit: false, // optional + rollback: false // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md index 5ec3e0541e..0aaec4e6cb 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md @@ -12,5 +12,6 @@ const result = await databases.upsertDocument({ collectionId: '', documentId: '', data: {}, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-documents.md b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-documents.md index 8deec536ff..16ed70fae9 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-documents.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-documents.md @@ -10,5 +10,6 @@ const databases = new sdk.Databases(client); const result = await databases.upsertDocuments({ databaseId: '', collectionId: '', - documents: [] + documents: [], + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/messaging/create-push.md b/docs/examples/1.8.x/server-nodejs/examples/messaging/create-push.md index c0a7f4713f..4c64813f25 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/messaging/create-push.md +++ b/docs/examples/1.8.x/server-nodejs/examples/messaging/create-push.md @@ -16,7 +16,7 @@ const result = await messaging.createPush({ targets: [], // optional data: {}, // optional action: '', // optional - image: '[ID1:ID2]', // optional + image: '', // optional icon: '', // optional sound: '', // optional color: '', // optional diff --git a/docs/examples/1.8.x/server-nodejs/examples/messaging/update-push.md b/docs/examples/1.8.x/server-nodejs/examples/messaging/update-push.md index 5e857f1ff6..6f5899f8c7 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/messaging/update-push.md +++ b/docs/examples/1.8.x/server-nodejs/examples/messaging/update-push.md @@ -16,7 +16,7 @@ const result = await messaging.updatePush({ body: '', // optional data: {}, // optional action: '', // optional - image: '[ID1:ID2]', // optional + image: '', // optional icon: '', // optional sound: '', // optional color: '', // optional diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-operations.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-operations.md new file mode 100644 index 0000000000..25492396dd --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-operations.md @@ -0,0 +1,23 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.createOperations({ + transactionId: '', + operations: [ + { + "action": "create", + "databaseId": "", + "tableId": "", + "rowId": "", + "data": { + "name": "Walter O'Brien" + } + } + ] // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md index 29ddab6652..4468c168e8 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md @@ -18,5 +18,6 @@ const result = await tablesDB.createRow({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-rows.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-rows.md index 61eb1d1db8..20807c1612 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-rows.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-rows.md @@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client); const result = await tablesDB.createRows({ databaseId: '', tableId: '', - rows: [] + rows: [], + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-transaction.md new file mode 100644 index 0000000000..249406e333 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.createTransaction({ + ttl: 60 // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/decrement-row-column.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/decrement-row-column.md index e3b13a887e..0310399239 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/decrement-row-column.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/decrement-row-column.md @@ -13,5 +13,6 @@ const result = await tablesDB.decrementRowColumn({ rowId: '', column: '', value: null, // optional - min: null // optional + min: null, // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-row.md index 9802f0d03e..68a965dc97 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-row.md @@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client); const result = await tablesDB.deleteRow({ databaseId: '', tableId: '', - rowId: '' + rowId: '', + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-rows.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-rows.md index 6f4d7cd68b..ce1d0f4ba1 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-rows.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-rows.md @@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client); const result = await tablesDB.deleteRows({ databaseId: '', tableId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-transaction.md new file mode 100644 index 0000000000..28d086b9cf --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/delete-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.deleteTransaction({ + transactionId: '' +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-row.md index 7ea3d1fca2..fe67e2fda3 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-row.md @@ -11,5 +11,6 @@ const result = await tablesDB.getRow({ databaseId: '', tableId: '', rowId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-transaction.md new file mode 100644 index 0000000000..208368bc00 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/get-transaction.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.getTransaction({ + transactionId: '' +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/increment-row-column.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/increment-row-column.md index f5cac2ebaa..aaa6cd6205 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/increment-row-column.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/increment-row-column.md @@ -13,5 +13,6 @@ const result = await tablesDB.incrementRowColumn({ rowId: '', column: '', value: null, // optional - max: null // optional + max: null, // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-rows.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-rows.md index 6d3b883bd4..3f29781a91 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-rows.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-rows.md @@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client); const result = await tablesDB.listRows({ databaseId: '', tableId: '', - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-transactions.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-transactions.md new file mode 100644 index 0000000000..3ad0c95425 --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/list-transactions.md @@ -0,0 +1,12 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.listTransactions({ + queries: [] // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md index 1c7f0af197..58583af745 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md @@ -12,5 +12,6 @@ const result = await tablesDB.updateRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-rows.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-rows.md index 31628d7150..d66fc28a62 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-rows.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-rows.md @@ -11,5 +11,6 @@ const result = await tablesDB.updateRows({ databaseId: '', tableId: '', data: {}, // optional - queries: [] // optional + queries: [], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-transaction.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-transaction.md new file mode 100644 index 0000000000..03501d2cbf --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-transaction.md @@ -0,0 +1,14 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setKey(''); // Your secret API key + +const tablesDB = new sdk.TablesDB(client); + +const result = await tablesDB.updateTransaction({ + transactionId: '', + commit: false, // optional + rollback: false // optional +}); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md index 9963bb6ced..bfb833356a 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md @@ -12,5 +12,6 @@ const result = await tablesDB.upsertRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"] // optional + permissions: ["read("any")"], // optional + transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-rows.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-rows.md index cca2b777bb..c985c9515b 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-rows.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-rows.md @@ -10,5 +10,6 @@ const tablesDB = new sdk.TablesDB(client); const result = await tablesDB.upsertRows({ databaseId: '', tableId: '', - rows: [] + rows: [], + transactionId: '' // optional }); From 42d981c0ef813d213f1fec5021c4fd6cc66f5e57 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 16 Sep 2025 13:15:08 +1200 Subject: [PATCH 102/145] Add scopes to roles --- app/config/roles.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/config/roles.php b/app/config/roles.php index 0f0945a2b4..6a4bba2699 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -16,6 +16,8 @@ $member = [ 'documents.write', 'rows.read', 'rows.write', + 'transactions.read', + 'transactions.write', 'files.read', 'files.write', 'projects.read', @@ -41,6 +43,8 @@ $admins = [ 'documents.write', 'rows.read', 'rows.write', + 'transactions.read', + 'transactions.write', 'files.read', 'files.write', 'buckets.read', From fb65d7a965d0dc263fa7c28f8709bb9f6b0522ca Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 16 Sep 2025 13:25:15 +1200 Subject: [PATCH 103/145] Add scoped tests --- .../{TransactionsTest.php => TransactionsBase.php} | 8 +------- .../Transactions/TransactionsConsoleClientTest.php | 13 +++++++++++++ .../Transactions/TransactionsCustomClientTest.php | 13 +++++++++++++ .../Transactions/TransactionsCustomServerTest.php | 13 +++++++++++++ .../{TransactionsTest.php => TransactionsBase.php} | 8 +------- .../Transactions/TransactionsConsoleClientTest.php | 13 +++++++++++++ .../Transactions/TransactionsCustomClientTest.php | 13 +++++++++++++ .../Transactions/TransactionsCustomServerTest.php | 13 +++++++++++++ 8 files changed, 80 insertions(+), 14 deletions(-) rename tests/e2e/Services/Databases/Legacy/Transactions/{TransactionsTest.php => TransactionsBase.php} (99%) create mode 100644 tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php create mode 100644 tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomClientTest.php create mode 100644 tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomServerTest.php rename tests/e2e/Services/Databases/TablesDB/Transactions/{TransactionsTest.php => TransactionsBase.php} (99%) create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsConsoleClientTest.php create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomClientTest.php create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomServerTest.php diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php similarity index 99% rename from tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php rename to tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php index a76d909b5e..f05563123c 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php @@ -3,19 +3,13 @@ namespace Tests\E2E\Services\Databases\Legacy\Transactions; use Tests\E2E\Client; -use Tests\E2E\Scopes\ProjectCustom; -use Tests\E2E\Scopes\Scope; -use Tests\E2E\Scopes\SideClient; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; -class TransactionsTest extends Scope +trait TransactionsBase { - use ProjectCustom; - use SideClient; - /** * Test creating a transaction */ diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php new file mode 100644 index 0000000000..427e79fb3a --- /dev/null +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php @@ -0,0 +1,13 @@ + Date: Tue, 16 Sep 2025 11:29:13 +0300 Subject: [PATCH 104/145] Add Scope for tests --- .../TablesDB/Transactions/TransactionsConsoleClientTest.php | 3 ++- .../TablesDB/Transactions/TransactionsCustomClientTest.php | 3 ++- .../TablesDB/Transactions/TransactionsCustomServerTest.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsConsoleClientTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsConsoleClientTest.php index 553c620f20..2159390fa2 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsConsoleClientTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsConsoleClientTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\TablesDB\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; -class TransactionsConsoleClientTest +class TransactionsConsoleClientTest extends Scope { use TransactionsBase; use ProjectCustom; diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomClientTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomClientTest.php index 3b7355faaf..732537fc7b 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomClientTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomClientTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\TablesDB\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; -class TransactionsCustomClientTest +class TransactionsCustomClientTest extends Scope { use TransactionsBase; use ProjectCustom; diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomServerTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomServerTest.php index 01d4a8d057..57588788b1 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomServerTest.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsCustomServerTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\TablesDB\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -class TransactionsCustomServerTest +class TransactionsCustomServerTest extends Scope { use TransactionsBase; use ProjectCustom; From b38b2bba0c81d184d67da5cf5ad78ea74494eb14 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 16 Sep 2025 21:13:41 +1200 Subject: [PATCH 105/145] Remove state order on created, rely on sequence order --- src/Appwrite/Databases/TransactionState.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 7d99d61d01..1caae8c629 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -31,7 +31,6 @@ class TransactionState // Fetch operations ordered by sequence to replay in exact order $operations = $this->dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc('$createdAt'), // Ensure operations are processed in order ]); $state = []; From 4a3cbdafca1deeb3ccd4b71254fa55eb9047e942 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 25 Sep 2025 17:59:39 +1200 Subject: [PATCH 106/145] Move to row-level scope for txn --- app/config/roles.php | 4 ---- app/config/scopes.php | 6 ------ app/config/specs/open-api3-1.8.x-client.json | 20 +++++++++---------- app/config/specs/open-api3-1.8.x-console.json | 20 +++++++++---------- app/config/specs/open-api3-1.8.x-server.json | 20 +++++++++---------- app/config/specs/open-api3-latest-client.json | 20 +++++++++---------- .../specs/open-api3-latest-console.json | 20 +++++++++---------- app/config/specs/open-api3-latest-server.json | 20 +++++++++---------- app/config/specs/swagger2-1.8.x-client.json | 20 +++++++++---------- app/config/specs/swagger2-1.8.x-console.json | 20 +++++++++---------- app/config/specs/swagger2-1.8.x-server.json | 20 +++++++++---------- app/config/specs/swagger2-latest-client.json | 20 +++++++++---------- app/config/specs/swagger2-latest-console.json | 20 +++++++++---------- app/config/specs/swagger2-latest-server.json | 20 +++++++++---------- .../Http/Databases/Transactions/Create.php | 2 +- .../Http/Databases/Transactions/Delete.php | 2 +- .../Transactions/Operations/Create.php | 2 +- .../Http/Databases/Transactions/Update.php | 2 +- .../Http/TablesDB/Transactions/Create.php | 2 +- .../Http/TablesDB/Transactions/Delete.php | 2 +- .../Http/TablesDB/Transactions/Get.php | 2 +- .../Transactions/Operations/Create.php | 2 +- .../Http/TablesDB/Transactions/Update.php | 2 +- .../Http/TablesDB/Transactions/XList.php | 2 +- tests/e2e/Scopes/ProjectCustom.php | 2 -- 25 files changed, 130 insertions(+), 142 deletions(-) diff --git a/app/config/roles.php b/app/config/roles.php index 6a4bba2699..0f0945a2b4 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -16,8 +16,6 @@ $member = [ 'documents.write', 'rows.read', 'rows.write', - 'transactions.read', - 'transactions.write', 'files.read', 'files.write', 'projects.read', @@ -43,8 +41,6 @@ $admins = [ 'documents.write', 'rows.read', 'rows.write', - 'transactions.read', - 'transactions.write', 'files.read', 'files.write', 'buckets.read', diff --git a/app/config/scopes.php b/app/config/scopes.php index 9807c09216..d90ca2eabf 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -52,12 +52,6 @@ return [ // List of publicly visible scopes 'indexes.write' => [ 'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes', ], - 'transactions.read' => [ - 'description' => 'Access to read your project\'s database transactions', - ], - 'transactions.write' => [ - 'description' => 'Access to create, update, and delete your project\'s database transactions', - ], 'documents.read' => [ 'description' => 'Access to read your project\'s database documents', ], diff --git a/app/config/specs/open-api3-1.8.x-client.json b/app/config/specs/open-api3-1.8.x-client.json index 6d5acf8c86..4fd19f7d6a 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -4903,7 +4903,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5033,7 +5033,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5109,7 +5109,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5173,7 +5173,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -7980,7 +7980,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -8045,7 +8045,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8113,7 +8113,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -8175,7 +8175,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8251,7 +8251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8315,7 +8315,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index 797a7e72e4..fe014dcd4c 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -5302,7 +5302,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5432,7 +5432,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5508,7 +5508,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5572,7 +5572,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -33372,7 +33372,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -33437,7 +33437,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33505,7 +33505,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -33567,7 +33567,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33643,7 +33643,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33707,7 +33707,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index 8e8c33ace4..16ae5e57f9 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -4842,7 +4842,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -4976,7 +4976,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5054,7 +5054,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5120,7 +5120,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -23888,7 +23888,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -23955,7 +23955,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24025,7 +24025,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -24089,7 +24089,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24167,7 +24167,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24233,7 +24233,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index 6d5acf8c86..4fd19f7d6a 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -4903,7 +4903,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5033,7 +5033,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5109,7 +5109,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5173,7 +5173,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -7980,7 +7980,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -8045,7 +8045,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8113,7 +8113,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -8175,7 +8175,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8251,7 +8251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -8315,7 +8315,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 797a7e72e4..fe014dcd4c 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -5302,7 +5302,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5432,7 +5432,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5508,7 +5508,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5572,7 +5572,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -33372,7 +33372,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -33437,7 +33437,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33505,7 +33505,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -33567,7 +33567,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33643,7 +33643,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -33707,7 +33707,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 8e8c33ace4..16ae5e57f9 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -4842,7 +4842,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -4976,7 +4976,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5054,7 +5054,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -5120,7 +5120,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client", @@ -23888,7 +23888,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -23955,7 +23955,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24025,7 +24025,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client", @@ -24089,7 +24089,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24167,7 +24167,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", @@ -24233,7 +24233,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client", diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index e87ce88a6c..1aa47cb4fc 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -5045,7 +5045,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5174,7 +5174,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5251,7 +5251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5314,7 +5314,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -8057,7 +8057,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -8122,7 +8122,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8190,7 +8190,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -8251,7 +8251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8328,7 +8328,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8391,7 +8391,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 16744679ec..f88b1254cd 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -5464,7 +5464,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5593,7 +5593,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5670,7 +5670,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5733,7 +5733,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -33491,7 +33491,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -33556,7 +33556,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33624,7 +33624,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -33685,7 +33685,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33762,7 +33762,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33825,7 +33825,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index d3f7129c95..40f38c1067 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -4992,7 +4992,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5125,7 +5125,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5204,7 +5204,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5269,7 +5269,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -24063,7 +24063,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -24130,7 +24130,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24200,7 +24200,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -24263,7 +24263,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24342,7 +24342,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24407,7 +24407,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index e87ce88a6c..1aa47cb4fc 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -5045,7 +5045,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5174,7 +5174,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5251,7 +5251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5314,7 +5314,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -8057,7 +8057,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -8122,7 +8122,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8190,7 +8190,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -8251,7 +8251,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8328,7 +8328,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -8391,7 +8391,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 16744679ec..f88b1254cd 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5464,7 +5464,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5593,7 +5593,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5670,7 +5670,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5733,7 +5733,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -33491,7 +33491,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -33556,7 +33556,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33624,7 +33624,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -33685,7 +33685,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33762,7 +33762,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -33825,7 +33825,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index d3f7129c95..40f38c1067 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -4992,7 +4992,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5125,7 +5125,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5204,7 +5204,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -5269,7 +5269,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "documents.write", "platforms": [ "server", "client" @@ -24063,7 +24063,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -24130,7 +24130,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24200,7 +24200,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.read", + "scope": "rows.read", "platforms": [ "server", "client" @@ -24263,7 +24263,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24342,7 +24342,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" @@ -24407,7 +24407,7 @@ "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", - "scope": "transactions.write", + "scope": "rows.write", "platforms": [ "server", "client" diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index 4a7e95d5ca..a5866ba7d1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -33,7 +33,7 @@ class Create extends Action ->setHttpPath('/v1/databases/transactions') ->desc('Create transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'documents.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php index da92ce1b4c..a5d2562572 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php @@ -32,7 +32,7 @@ class Delete extends Action ->setHttpPath('/v1/databases/transactions/:transactionId') ->desc('Delete transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'documents.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index d2e438706a..0aca3e3f7c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -38,7 +38,7 @@ class Create extends Action ->setHttpPath('/v1/databases/transactions/:transactionId/operations') ->desc('Create operations scoped to a transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'documents.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 4509114bfc..9f5e5037bf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -44,7 +44,7 @@ class Update extends Action ->setHttpPath('/v1/databases/transactions/:transactionId') ->desc('Update transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'documents.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php index 6a28d31621..e6c24b3341 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Create.php @@ -30,7 +30,7 @@ class Create extends TransactionsCreate ->setHttpPath('/v1/tablesdb/transactions') ->desc('Create transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'rows.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php index cad11e795a..28f273f566 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php @@ -30,7 +30,7 @@ class Delete extends TransactionsDelete ->setHttpPath('/v1/tablesdb/transactions/:transactionId') ->desc('Delete transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'rows.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php index 39f6754a1e..bc58783a04 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Get.php @@ -30,7 +30,7 @@ class Get extends TransactionsGet ->setHttpPath('/v1/tablesdb/transactions/:transactionId') ->desc('Get transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.read') + ->label('scope', 'rows.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php index 2280a6f7e3..eab0fed161 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -32,7 +32,7 @@ class Create extends OperationsCreate ->setHttpPath('/v1/tablesdb/transactions/:transactionId/operations') ->desc('Create operations scoped to a transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'rows.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index d76de0766d..bcfb2db406 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -31,7 +31,7 @@ class Update extends TransactionsUpdate ->setHttpPath('/v1/tablesdb/transactions/:transactionId') ->desc('Update transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.write') + ->label('scope', 'rows.write') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php index 67a58d7e5f..cfb630e46d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/XList.php @@ -30,7 +30,7 @@ class XList extends TransactionsList ->setHttpPath('/v1/tablesdb/transactions') ->desc('List transactions') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.read') + ->label('scope', 'rows.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'tablesDB', diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 49aabaae67..c2b4896814 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -70,8 +70,6 @@ trait ProjectCustom 'databases.write', 'collections.read', 'collections.write', - 'transactions.read', - 'transactions.write', 'tables.read', 'tables.write', 'documents.read', From 7a03084e832a57327e8d3244c25cef1122b98fd5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 25 Sep 2025 19:21:21 +1200 Subject: [PATCH 107/145] Bump SDK version --- app/config/platforms.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index 1eb8891b8a..fa2ec09ee2 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -11,7 +11,7 @@ return [ [ 'key' => 'web', 'name' => 'Web', - 'version' => '20.1.0-rc.1', + 'version' => '20.2.0-rc.1', 'url' => 'https://github.com/appwrite/sdk-for-web', 'package' => 'https://www.npmjs.com/package/appwrite', 'enabled' => true, @@ -262,7 +262,7 @@ return [ [ 'key' => 'nodejs', 'name' => 'Node.js', - 'version' => '19.1.0-rc.1', + 'version' => '19.2.0-rc.1', 'url' => 'https://github.com/appwrite/sdk-for-node', 'package' => 'https://www.npmjs.com/package/node-appwrite', 'enabled' => true, From e06c358db494943af8324015059afd194661be23 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 26 Sep 2025 00:50:25 +1200 Subject: [PATCH 108/145] Don't require data for delete operation --- .../Utopia/Database/Validator/Operation.php | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 4632678940..6bc068a60f 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -24,6 +24,20 @@ class Operation extends Validator 'decrement' => true, ]; + /** @var array */ + private array $requiresData = [ + 'create' => true, + 'update' => true, + 'upsert' => true, + 'delete' => false, // Delete doesn't need data + 'increment' => true, + 'decrement' => true, + 'bulkCreate' => true, + 'bulkUpdate' => true, + 'bulkUpsert' => true, + 'bulkDelete' => true, + ]; + /** @var array */ private array $actions = [ 'create' => true, @@ -121,14 +135,23 @@ class Operation extends Validator return false; } - // Data must be present and must be array (can be empty) - if (!\array_key_exists('data', $value)) { - $this->description = "Missing required key: data"; - return false; - } - if (!\is_array($value['data'])) { - $this->description = "Key 'data' must be an array"; - return false; + // Data validation - only required for certain actions + if (isset($this->requiresData[$value['action']]) && $this->requiresData[$value['action']]) { + // Data is required for this action + if (!\array_key_exists('data', $value)) { + $this->description = "Missing required key: data"; + return false; + } + if (!\is_array($value['data'])) { + $this->description = "Key 'data' must be an array"; + return false; + } + } else if (\array_key_exists('data', $value)) { + // Data is optional but if provided, must be an array + if (!\is_array($value['data'])) { + $this->description = "Key 'data' must be an array"; + return false; + } } return true; From 4e91904608d4d1269a26458e9c2ab229976378e9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 26 Sep 2025 14:25:02 +1200 Subject: [PATCH 109/145] Update database --- composer.lock | 60 +++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/composer.lock b/composer.lock index 1ff64ab5bc..d8fb2b1b4a 100644 --- a/composer.lock +++ b/composer.lock @@ -1569,16 +1569,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.8.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "ce27936c8dfb73e3ab9c94469130428af9752c96" + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/ce27936c8dfb73e3ab9c94469130428af9752c96", - "reference": "ce27936c8dfb73e3ab9c94469130428af9752c96", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", "shasum": "" }, "require": { @@ -1632,7 +1632,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2025-09-22T20:41:46+00:00" + "time": "2025-09-24T15:12:37+00:00" }, { "name": "paragonie/random_compat", @@ -3635,16 +3635,16 @@ }, { "name": "utopia-php/database", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "732ffef52fd76483722537ee1f4f83726125a7ec" + "reference": "19a3ab2ae99578861dd1a7b4fdbfb62b37d09447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/732ffef52fd76483722537ee1f4f83726125a7ec", - "reference": "732ffef52fd76483722537ee1f4f83726125a7ec", + "url": "https://api.github.com/repos/utopia-php/database/zipball/19a3ab2ae99578861dd1a7b4fdbfb62b37d09447", + "reference": "19a3ab2ae99578861dd1a7b4fdbfb62b37d09447", "shasum": "" }, "require": { @@ -3685,9 +3685,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/2.1.0" + "source": "https://github.com/utopia-php/database/tree/2.2.0" }, - "time": "2025-09-16T13:44:45+00:00" + "time": "2025-09-26T02:23:30+00:00" }, { "name": "utopia-php/detector", @@ -3939,16 +3939,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.27", + "version": "0.33.28", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d9d10a895e85c8c7675220347cc6109db9d3bd37" + "reference": "5aaa94d406577b0059ad28c78022606890dc6de0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/d9d10a895e85c8c7675220347cc6109db9d3bd37", - "reference": "d9d10a895e85c8c7675220347cc6109db9d3bd37", + "url": "https://api.github.com/repos/utopia-php/http/zipball/5aaa94d406577b0059ad28c78022606890dc6de0", + "reference": "5aaa94d406577b0059ad28c78022606890dc6de0", "shasum": "" }, "require": { @@ -3980,9 +3980,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.27" + "source": "https://github.com/utopia-php/http/tree/0.33.28" }, - "time": "2025-09-07T18:40:53+00:00" + "time": "2025-09-25T10:44:24+00:00" }, { "name": "utopia-php/image", @@ -6230,16 +6230,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.27", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0a9aa4440b6a9528cf360071502628d717af3e0a", - "reference": "0a9aa4440b6a9528cf360071502628d717af3e0a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -6264,7 +6264,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -6313,7 +6313,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.27" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -6337,7 +6337,7 @@ "type": "tidelift" } ], - "time": "2025-09-14T06:18:03+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "psr/cache", @@ -6829,16 +6829,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.7", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "eb49b981ef0817890129cb70f774506bebe57740" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/eb49b981ef0817890129cb70f774506bebe57740", - "reference": "eb49b981ef0817890129cb70f774506bebe57740", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -6894,7 +6894,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.7" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { @@ -6914,7 +6914,7 @@ "type": "tidelift" } ], - "time": "2025-09-22T05:18:21+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", From ef147cde21604a76b860d90b3ac50506fc1efef4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 26 Sep 2025 14:28:11 +1200 Subject: [PATCH 110/145] Format --- src/Appwrite/Platform/Workers/StatsUsage.php | 2 +- src/Appwrite/Utopia/Database/Validator/Operation.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index e7e8dba4f3..a2473420a4 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -435,7 +435,7 @@ class StatsUsage extends Action return $cmp; } - unset($this->projects[$sequence]); + unset($this->projects[$sequence]); // Period ASC $cmp = strcmp($a['period'], $b['period']); if ($cmp !== 0) { diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 6bc068a60f..a8de32de79 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -146,7 +146,7 @@ class Operation extends Validator $this->description = "Key 'data' must be an array"; return false; } - } else if (\array_key_exists('data', $value)) { + } elseif (\array_key_exists('data', $value)) { // Data is optional but if provided, must be an array if (!\is_array($value['data'])) { $this->description = "Key 'data' must be an array"; From 372a50f9a573e5410134501dffd609b540c03356 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 26 Sep 2025 14:45:09 +1200 Subject: [PATCH 111/145] Update lock --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 0225722352..d8fb2b1b4a 100644 --- a/composer.lock +++ b/composer.lock @@ -4187,16 +4187,16 @@ }, { "name": "utopia-php/migration", - "version": "1.2.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093" + "reference": "6fb6f8f032cd34c3c65728a55d494adeac2ff038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/42ff497c5231f5a727d1e229419ff1d2195d8093", - "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/6fb6f8f032cd34c3c65728a55d494adeac2ff038", + "reference": "6fb6f8f032cd34c3c65728a55d494adeac2ff038", "shasum": "" }, "require": { @@ -4237,9 +4237,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.2.0" + "source": "https://github.com/utopia-php/migration/tree/1.1.0" }, - "time": "2025-09-24T10:32:24+00:00" + "time": "2025-09-10T05:45:30+00:00" }, { "name": "utopia-php/orchestration", From 26ae33522df1159d9d1b6ab4135d6fbadf7f3bd0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 21:36:33 +1300 Subject: [PATCH 112/145] Fix txn read scope --- .../Modules/Databases/Http/Databases/Transactions/Get.php | 2 +- .../Modules/Databases/Http/Databases/Transactions/XList.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php index 43406a6751..1d4d22baa1 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Get.php @@ -31,7 +31,7 @@ class Get extends Action ->setHttpPath('/v1/databases/transactions/:transactionId') ->desc('Get transaction') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.read') + ->label('scope', 'rows.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php index 05ef5ae9e8..33c66b90c7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/XList.php @@ -34,7 +34,7 @@ class XList extends Action ->setHttpPath('/v1/databases/transactions') ->desc('List transactions') ->groups(['api', 'database', 'transactions']) - ->label('scope', 'transactions.read') + ->label('scope', 'rows.read') ->label('resourceType', RESOURCE_TYPE_DATABASES) ->label('sdk', new Method( namespace: 'databases', From ae661e996157e98230a0bb9d2508cf4e2afb9057 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 21:37:54 +1300 Subject: [PATCH 113/145] Fix missing bulk map --- .../Modules/Databases/Http/Databases/Transactions/Update.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 9f5e5037bf..214052a173 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -545,7 +545,7 @@ class Update extends Action $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { $dbForProject->createDocuments( $collectionId, - $data, + \array_map(fn($doc) => new Document($doc), $data), onNext: function (Document $document) use (&$state, $collectionId) { $state[$collectionId][$document->getId()] = $document; } @@ -603,7 +603,7 @@ class Update extends Action // Run bulk upsert without timestamp wrapper, checking manually in callback $dbForProject->upsertDocuments( $collectionId, - $data, + \array_map(fn($doc) => new Document($doc), $data), onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) { if ($old !== null) { // This is an update - check if document was created/modified in this transaction From 490341af090b1672fb7fa7c7fcdbfaf3c499d33e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 21:46:28 +1300 Subject: [PATCH 114/145] Fix log limit --- .../Modules/Databases/Http/Databases/Transactions/Update.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 214052a173..ac2efc31cf 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -133,6 +133,7 @@ class Update extends Action // Fetch operations ordered by sequence by default to replay operations in exact order they were created $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::limit(PHP_INT_MAX), ]); // Track transaction state for cross-operation visibility From f667149556f1530e0f01f56f2a52b25b657e1bc2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 22:12:02 +1300 Subject: [PATCH 115/145] Trigger CI From 473be9284d8c3fb0767fd0df8ced1681a12906bd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 22:24:54 +1300 Subject: [PATCH 116/145] Check convert --- .../Http/Databases/Transactions/Update.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index ac2efc31cf..afa466c366 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -544,9 +544,14 @@ class Update extends Action array &$state ): void { $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { + // Convert data arrays to Document objects if needed + $documents = \array_map(function ($doc) { + return $doc instanceof Document ? $doc : new Document($doc); + }, $data); + $dbForProject->createDocuments( $collectionId, - \array_map(fn($doc) => new Document($doc), $data), + $documents, onNext: function (Document $document) use (&$state, $collectionId) { $state[$collectionId][$document->getId()] = $document; } @@ -601,10 +606,15 @@ class Update extends Action \DateTime $createdAt, array &$state ): void { + // Convert data arrays to Document objects if needed + $documents = \array_map(function ($doc) { + return $doc instanceof Document ? $doc : new Document($doc); + }, $data); + // Run bulk upsert without timestamp wrapper, checking manually in callback $dbForProject->upsertDocuments( $collectionId, - \array_map(fn($doc) => new Document($doc), $data), + $documents, onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) { if ($old !== null) { // This is an update - check if document was created/modified in this transaction From 93d8f1cfe7f3eb66fc6c05b3236421090231978e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:03:52 +1300 Subject: [PATCH 117/145] Update src/Appwrite/Databases/TransactionState.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Appwrite/Databases/TransactionState.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 1caae8c629..495a7b70ef 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -49,15 +49,15 @@ class TransactionState switch ($action) { case 'create': - if ($documentId) { - $state[$collectionId][$documentId] = [ + $docId = $documentId ?? ($data['$id'] ?? null); + if ($docId) { + $state[$collectionId][$docId] = [ 'action' => 'create', 'document' => new Document($data), 'exists' => true ]; } break; - case 'update': if (isset($state[$collectionId][$documentId])) { // Update existing document in transaction state From 9813b98133bc0682c195a8c6bc5f64329cfb056d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:05:43 +1300 Subject: [PATCH 118/145] Update src/Appwrite/Databases/TransactionState.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Appwrite/Databases/TransactionState.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 495a7b70ef..c20eac3d1a 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -79,7 +79,9 @@ class TransactionState break; case 'upsert': - $state[$collectionId][$documentId] = [ + $docId = $documentId ?? ($data['$id'] ?? null); + if (!$docId) { break; } + $state[$collectionId][$docId] = [ 'action' => 'upsert', 'document' => new Document($data), 'exists' => true From 51d5ddb4637d0de70e21eb2ccc96842f40ad0415 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:17:17 +1300 Subject: [PATCH 119/145] Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6123454a8b..e5ae97ba7c 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 @@ -124,7 +124,7 @@ class Upsert extends Action 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 + 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch ); } From 6581d68e6d9daf40b7cbf44f065fdb64862ac477 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:20:02 +1300 Subject: [PATCH 120/145] Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e5ae97ba7c..2c8d26020d 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 @@ -144,7 +144,7 @@ class Upsert extends Action 'transactions', $transactionId, 'operations', - \count($staged) + 1 ); }); From a9a97b2fe0b735fa006d7f3d9bbd346be41f0709 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:20:22 +1300 Subject: [PATCH 121/145] Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Databases/Http/Databases/Collections/Documents/Create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 67a54068b8..902a3585ba 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -381,7 +381,7 @@ class Create extends Action 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 + 'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch ); } From 45da32d5d9129c8b0815d2ec0b7a82d2af9e82e2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 2 Oct 2025 23:57:30 +1300 Subject: [PATCH 122/145] Update src/Appwrite/Utopia/Database/Validator/Operation.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Utopia/Database/Validator/Operation.php | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index a8de32de79..5cc3ca2135 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -127,14 +127,25 @@ class Operation extends Validator } // If action requires documentId, it must be present - if ( - isset($this->requiresDocumentId[$value['action']]) && - !\array_key_exists($this->documentIdName, $value) - ) { +- if ( +- isset($this->requiresDocumentId[$value['action']]) && +- !\array_key_exists($this->documentIdName, $value) +- ) { +- $this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'"; +- return false; + $actionRequiresDocumentId = ($this->requiresDocumentId[$value['action']] ?? false) === true; + if ($actionRequiresDocumentId && !\array_key_exists($this->documentIdName, $value)) { $this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'"; return false; } + if (\array_key_exists($this->documentIdName, $value)) { + if (!\is_string($value[$this->documentIdName]) || \trim($value[$this->documentIdName]) === '') { + $this->description = "Key '$this->documentIdName' must be a non-empty string"; + return false; + } + } + // Data validation - only required for certain actions if (isset($this->requiresData[$value['action']]) && $this->requiresData[$value['action']]) { // Data is required for this action @@ -146,7 +157,7 @@ class Operation extends Validator $this->description = "Key 'data' must be an array"; return false; } - } elseif (\array_key_exists('data', $value)) { + } else if (\array_key_exists('data', $value)) { // Data is optional but if provided, must be an array if (!\is_array($value['data'])) { $this->description = "Key 'data' must be an array"; From 36a8365ebbea6457cb53ea57d18af34599244930 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:03:20 +1300 Subject: [PATCH 123/145] Count from state --- src/Appwrite/Databases/TransactionState.php | 265 +++++++++++++++++- .../Databases/Collections/Documents/XList.php | 2 +- 2 files changed, 258 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 1caae8c629..6d51356f89 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -18,6 +18,56 @@ class TransactionState $this->dbForProject = $dbForProject; } + /** + * Apply projection (select) semantics from queries to a document + */ + private function applyProjection(Document $doc, array $queries): Document + { + if (empty($queries)) { + return $doc; + } + + // Extract selections from queries + $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 no selections or wildcard present, return document as-is + 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; + } + /** * Get the current state of a transaction by replaying its operations */ @@ -67,7 +117,11 @@ class TransactionState $existingDocument->setAttribute($key, $value); } } - $state[$collectionId][$documentId]['action'] = 'update'; + // 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 { // Document doesn't exist in transaction state, will be merged with committed version $state[$collectionId][$documentId] = [ @@ -141,7 +195,7 @@ class TransactionState if ($docState['action'] === 'create') { // Document was created in transaction, return the created version - return $docState['document']; + return $this->applyProjection($docState['document'], $queries); } if ($docState['action'] === 'update' || $docState['action'] === 'upsert') { @@ -154,10 +208,12 @@ class TransactionState $committedDoc->setAttribute($key, $value); } } - return $committedDoc; + // committedDoc already has projection applied by dbForProject->getDocument() + // But we need to reapply in case transaction added new fields + return $this->applyProjection($committedDoc, $queries); } elseif ($docState['action'] === 'upsert') { // Upsert created a new document since committed doc doesn't exist - return $docState['document']; + return $this->applyProjection($docState['document'], $queries); } } } @@ -195,8 +251,8 @@ class TransactionState // Document was deleted, remove from results unset($documentMap[$docId]); } elseif ($docState['action'] === 'create') { - // Document was created, add to results - $documentMap[$docId] = $docState['document']; + // 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 @@ -205,9 +261,11 @@ class TransactionState $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 - $documentMap[$docId] = $docState['document']; + // Upsert created a new document, apply projection + $documentMap[$docId] = $this->applyProjection($docState['document'], $queries); } } } @@ -216,6 +274,197 @@ class TransactionState return array_values($documentMap); } + /** + * Count documents with transaction-aware logic + */ + public function countDocuments( + string $collectionId, + ?string $transactionId = null, + array $queries = [] + ): int { + // If no transaction, use normal database count + if ($transactionId === null) { + return $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT); + } + + $state = $this->getTransactionState($transactionId); + + // Get base count from database + $baseCount = $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT); + + // If no transaction state for this collection, return base count + if (!isset($state[$collectionId])) { + return $baseCount; + } + + // Build a set of committed document IDs that match the query + // We need to find which documents match the filters + $committedDocs = $this->dbForProject->find($collectionId, $queries); + $committedDocIds = []; + foreach ($committedDocs as $doc) { + $committedDocIds[$doc->getId()] = true; + } + + $adjustedCount = $baseCount; + + // Apply transaction state changes to the count + foreach ($state[$collectionId] as $docId => $docState) { + if (!$docState['exists']) { + // Document was deleted in transaction + if (isset($committedDocIds[$docId])) { + $adjustedCount--; // Was in results, now deleted + } + } elseif ($docState['action'] === 'create') { + // Document was created in transaction + // We need to check if it would match the query filters + // For now, we'll conservatively add it if no filters are present + // or apply basic filter matching + if ($this->documentMatchesFilters($docState['document'], $queries)) { + $adjustedCount++; // New document that matches + } + } elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') { + // Document was updated/upserted + $wasInResults = isset($committedDocIds[$docId]); + $nowMatches = $this->documentMatchesFilters($docState['document'], $queries); + + if (!$wasInResults && $nowMatches && $docState['action'] === 'upsert') { + $adjustedCount++; // Upsert created new document that matches + } elseif ($wasInResults && !$nowMatches) { + $adjustedCount--; // Update/upsert made document no longer match + } 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 matches filter queries (simplified implementation) + */ + private function documentMatchesFilters(Document $doc, array $queries): bool + { + // Extract filter queries + $filters = []; + foreach ($queries as $query) { + $method = $query->getMethod(); + // Only process filter queries, not limit/offset/cursor/select + 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; + } + } + + // If no filters, document matches + if (empty($filters)) { + return true; + } + + // Check each filter + 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_THAN: + if (!($docValue > $values[0])) { + return false; + } + break; + case Query::TYPE_GREATER_THAN_EQUAL: + if (!($docValue >= $values[0])) { + return false; + } + break; + case Query::TYPE_LESSER_THAN: + if (!($docValue < $values[0])) { + return false; + } + break; + case Query::TYPE_LESSER_THAN_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; + } + /** * Check if a document exists with transaction-aware logic */ diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 26d315e52e..a30fed47ed 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -129,7 +129,7 @@ class XList extends Action // Use transaction-aware document retrieval if transactionId is provided if ($transactionId !== null) { $documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries); - $total = count($documents); // For transaction-aware queries, we count the actual results + $total = $transactionState->countDocuments($collectionTableId, $transactionId, $queries); } elseif (! empty($selectQueries)) { // has selects, allow relationship on documents $documents = $dbForProject->find($collectionTableId, $queries); From 7fd2502dd5cc28364be986c4bf2e8c89f954579c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:03:34 +1300 Subject: [PATCH 124/145] Block client bulk upsert txn --- .../Databases/Http/Databases/Transactions/Operations/Create.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 0aca3e3f7c..20cf79551c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -87,6 +87,7 @@ class Create extends Action if (!$isAPIKey && !$isPrivilegedUser && \in_array($operation['action'], [ 'bulkCreate', 'bulkUpdate', + 'bulkUpsert', 'bulkDelete' ])) { throw new Exception(Exception::USER_UNAUTHORIZED); From f4830b1672f624b7e5ad3738c2e3859b2b4140be Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:03:46 +1300 Subject: [PATCH 125/145] Catch query exception --- .../Databases/Http/Databases/Transactions/Update.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index afa466c366..2513c09c64 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -18,6 +18,7 @@ use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; +use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; @@ -227,6 +228,11 @@ class Update extends Action 'status' => 'failed', ])); throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); + } catch (QueryException $e) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } }); From 5da2ca292af649ac6034bd8e9c26a16ce84e3847 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:03:54 +1300 Subject: [PATCH 126/145] Add missing injection --- .../Modules/Databases/Http/TablesDB/Transactions/Delete.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php index 28f273f566..46cd6b6c51 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php @@ -49,6 +49,7 @@ class Delete extends TransactionsDelete ->param('transactionId', '', new UID(), 'Transaction ID.') ->inject('response') ->inject('dbForProject') + ->inject('queueForDeletes') ->callback($this->action(...)); } } From d54058b59ade2721520beda1d8b809d2d36a7a29 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:12:19 +1300 Subject: [PATCH 127/145] Fix multi API event --- .../Databases/Http/Databases/Transactions/Update.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 2513c09c64..37642a0cca 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -266,12 +266,18 @@ class Update extends Action Query::equal('$sequence', [$collectionInternalId]) ]); + $groupId = $this->getGroupId(); + $resourceId = $this->getResourceId(); + $contextKey = $this->getContext(); + $resource = $this->getResource(); + $resourcePlural = $resource . 's'; + $queueForEvents ->setParam('databaseId', $database->getId()) ->setContext('database', $database) ->setParam('collectionId', $collection->getId()) ->setParam('tableId', $collection->getId()) - ->setContext('collection', $collection); + ->setContext($contextKey, $collection); $eventAction = ''; $documents = []; @@ -308,7 +314,7 @@ class Update extends Action $queueForEvents ->setParam('documentId', $docId) ->setParam('rowId', $docId) - ->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].' . $eventAction); + ->setEvent("databases.[databaseId].{$contextKey}s.[{$groupId}].{$resourcePlural}.[{$resourceId}]." . $eventAction); $queueForRealtime->from($queueForEvents)->trigger(); $queueForFunctions->from($queueForEvents)->trigger(); From 44e7068a9a0fc49f15a666b00b4975eb193a4941 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:26:39 +1300 Subject: [PATCH 128/145] Fix syntax --- src/Appwrite/Utopia/Database/Validator/Operation.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 5cc3ca2135..911250c2bc 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -127,12 +127,6 @@ class Operation extends Validator } // If action requires documentId, it must be present -- if ( -- isset($this->requiresDocumentId[$value['action']]) && -- !\array_key_exists($this->documentIdName, $value) -- ) { -- $this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'"; -- return false; $actionRequiresDocumentId = ($this->requiresDocumentId[$value['action']] ?? false) === true; if ($actionRequiresDocumentId && !\array_key_exists($this->documentIdName, $value)) { $this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'"; From 522a4d2a62ab781526a8df9ba442519fcb4914e3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 00:39:14 +1300 Subject: [PATCH 129/145] Format --- src/Appwrite/Databases/TransactionState.php | 4 +++- .../Http/Databases/Collections/Documents/Bulk/Upsert.php | 2 +- src/Appwrite/Utopia/Database/Validator/Operation.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index f37366f72c..1f8e7f65c8 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -134,7 +134,9 @@ class TransactionState case 'upsert': $docId = $documentId ?? ($data['$id'] ?? null); - if (!$docId) { break; } + if (!$docId) { + break; + } $state[$collectionId][$docId] = [ 'action' => 'upsert', 'document' => new Document($data), 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 2c8d26020d..a2156484a8 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 @@ -144,7 +144,7 @@ class Upsert extends Action 'transactions', $transactionId, 'operations', - 1 + 1 ); }); diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 911250c2bc..2e3dc8e9e0 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -151,7 +151,7 @@ class Operation extends Validator $this->description = "Key 'data' must be an array"; return false; } - } else if (\array_key_exists('data', $value)) { + } elseif (\array_key_exists('data', $value)) { // Data is optional but if provided, must be an array if (!\is_array($value['data'])) { $this->description = "Key 'data' must be an array"; From 71b2164154ae844a6c4bbe024da2c03f8689ae0e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 16:58:23 +1300 Subject: [PATCH 130/145] Clean up state helper --- src/Appwrite/Databases/TransactionState.php | 484 +++++++++++------- .../Http/Databases/Transactions/Update.php | 117 ++++- 2 files changed, 423 insertions(+), 178 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 1f8e7f65c8..85ecb41ae2 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -8,6 +8,11 @@ use Utopia\Database\Query; /** * 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 { @@ -18,161 +23,15 @@ class TransactionState $this->dbForProject = $dbForProject; } - /** - * Apply projection (select) semantics from queries to a document - */ - private function applyProjection(Document $doc, array $queries): Document - { - if (empty($queries)) { - return $doc; - } - - // Extract selections from queries - $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 no selections or wildcard present, return document as-is - 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; - } - - /** - * Get the current state of a transaction by replaying its operations - */ - private function getTransactionState(string $transactionId): array - { - $transaction = $this->dbForProject->getDocument('transactions', $transactionId); - if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') { - return []; - } - - // Fetch operations ordered by sequence to replay in exact order - $operations = $this->dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - ]); - - $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) { - $state[$collectionId][$docId] = [ - 'action' => 'create', - 'document' => new Document($data), - 'exists' => true - ]; - } - break; - case 'update': - if (isset($state[$collectionId][$documentId])) { - // Update existing document in transaction state - $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 { - // Document doesn't exist in transaction state, will be merged with committed version - $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 '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; - } - } - - return $state; - } /** * 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 */ public function getDocument( string $collectionId, @@ -187,7 +46,6 @@ class TransactionState $state = $this->getTransactionState($transactionId); - // Check if document exists in transaction state if (isset($state[$collectionId][$documentId])) { $docState = $state[$collectionId][$documentId]; @@ -212,8 +70,7 @@ class TransactionState $committedDoc->setAttribute($key, $value); } } - // committedDoc already has projection applied by dbForProject->getDocument() - // But we need to reapply in case transaction added new fields + // Reapply projection in case transaction added new fields return $this->applyProjection($committedDoc, $queries); } elseif ($docState['action'] === 'upsert') { // Upsert created a new document since committed doc doesn't exist @@ -228,6 +85,11 @@ class TransactionState /** * 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 */ public function listDocuments( string $collectionId, @@ -280,6 +142,11 @@ class TransactionState /** * 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 */ public function countDocuments( string $collectionId, @@ -302,7 +169,6 @@ class TransactionState } // Build a set of committed document IDs that match the query - // We need to find which documents match the filters $committedDocs = $this->dbForProject->find($collectionId, $queries); $committedDocIds = []; foreach ($committedDocs as $doc) { @@ -320,9 +186,6 @@ class TransactionState } } elseif ($docState['action'] === 'create') { // Document was created in transaction - // We need to check if it would match the query filters - // For now, we'll conservatively add it if no filters are present - // or apply basic filter matching if ($this->documentMatchesFilters($docState['document'], $queries)) { $adjustedCount++; // New document that matches } @@ -348,7 +211,285 @@ class TransactionState } /** - * Check if a document matches filter queries (simplified implementation) + * 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; + } + + foreach ($state[$collectionId] as $docId => $doc) { + if ($this->documentMatchesFilters($doc, $queries)) { + // Apply the update to the state document + 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; + } + + foreach ($state[$collectionId] as $docId => $doc) { + if ($this->documentMatchesFilters($doc, $queries)) { + unset($state[$collectionId][$docId]); + } + } + } + + /** + * Apply bulk upsert to documents in transaction state + * + * This allows bulk operations within a transaction to see each other's changes. + * + * @param string $collectionId Collection ID + * @param array $documents Array of Document objects to upsert + * @param array &$state Transaction state (passed by reference) + * @return void + */ + public function applyBulkUpsertToState( + string $collectionId, + array $documents, + array &$state + ): void { + foreach ($documents as $doc) { + if (!($doc instanceof Document)) { + continue; + } + + $docId = $doc->getId(); + if (!$docId) { + continue; + } + + // If document exists in state, update it; otherwise it will be handled by DB upsert + if (isset($state[$collectionId][$docId])) { + // Apply updates to existing state document + foreach ($doc->getArrayCopy() as $key => $value) { + if ($key !== '$id') { + $state[$collectionId][$docId]->setAttribute($key, $value); + } + } + } + } + } + + /** + * 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' => ...]]] + */ + private function getTransactionState(string $transactionId): array + { + $transaction = $this->dbForProject->getDocument('transactions', $transactionId); + if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') { + return []; + } + + // Fetch operations ordered by sequence to replay in exact order + $operations = $this->dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + ]); + + $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) { + $state[$collectionId][$docId] = [ + 'action' => 'create', + 'document' => new Document($data), + 'exists' => true + ]; + } + break; + + case 'update': + if (isset($state[$collectionId][$documentId])) { + // Update existing document in transaction state + $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 { + // Document doesn't exist in transaction state, will be merged with committed version + $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 '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; + } + } + + 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; + } + + // Extract selections from queries + $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 no selections or wildcard present, return document as-is + 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; + } + + /** + * Check if a document matches filter queries + * + * @param Document $doc Document to check + * @param array $queries Query filters + * @return bool True if document matches all filters */ private function documentMatchesFilters(Document $doc, array $queries): bool { @@ -387,11 +528,13 @@ class TransactionState 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) { @@ -404,6 +547,7 @@ class TransactionState return false; } break; + case Query::TYPE_STARTS_WITH: $matches = false; foreach ($values as $value) { @@ -416,6 +560,7 @@ class TransactionState return false; } break; + case Query::TYPE_ENDS_WITH: $matches = false; foreach ($values as $value) { @@ -428,36 +573,43 @@ class TransactionState return false; } break; + case Query::TYPE_GREATER_THAN: if (!($docValue > $values[0])) { return false; } break; + case Query::TYPE_GREATER_THAN_EQUAL: if (!($docValue >= $values[0])) { return false; } break; + case Query::TYPE_LESSER_THAN: if (!($docValue < $values[0])) { return false; } break; + case Query::TYPE_LESSER_THAN_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; @@ -468,16 +620,4 @@ class TransactionState return true; } - - /** - * Check if a document exists with transaction-aware logic - */ - public function documentExists( - string $collectionId, - string $documentId, - ?string $transactionId = null - ): bool { - $doc = $this->getDocument($collectionId, $documentId, $transactionId); - return !$doc->isEmpty(); - } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 37642a0cca..19ba8f10d8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; +use Appwrite\Databases\TransactionState; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; @@ -66,6 +67,7 @@ class Update extends Action ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) ->inject('response') ->inject('dbForProject') + ->inject('transactionState') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('queueForStatsUsage') @@ -81,6 +83,7 @@ class Update extends Action * @param bool $rollback * @param UtopiaResponse $response * @param Database $dbForProject + * @param TransactionState $transactionState * @param Delete $queueForDeletes * @param Event $queueForEvents * @param StatsUsage $queueForStatsUsage @@ -96,7 +99,7 @@ class Update extends Action * @throws Structure * @throws \Utopia\Exception */ - public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void + public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); @@ -182,13 +185,13 @@ class Update extends Action $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state); break; case 'bulkUpdate': - $this->handleBulkUpdateOperation($dbForProject, $collectionId, $data, $createdAt, $state); + $this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state); break; case 'bulkUpsert': - $this->handleBulkUpsertOperation($dbForProject, $collectionId, $data, $createdAt, $state); + $this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state); break; case 'bulkDelete': - $this->handleBulkDeleteOperation($dbForProject, $collectionId, $data, $createdAt, $state); + $this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state); break; } } @@ -348,6 +351,14 @@ class Update extends Action /** * Handle create operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string|null $documentId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws \Utopia\Database\Exception */ private function handleCreateOperation( @@ -371,6 +382,14 @@ class Update extends Action /** * Handle update operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string $documentId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws ConflictException * @throws \Utopia\Database\Exception */ @@ -410,6 +429,14 @@ class Update extends Action /** * Handle upsert operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string|null $documentId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws \Utopia\Database\Exception */ private function handleUpsertOperation( @@ -442,6 +469,15 @@ class Update extends Action /** * Handle delete operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string $documentId + * @param \DateTime $createdAt + * @param array &$state + * @return void + * @throws \Utopia\Database\Exception + * @throws NotFoundException */ private function handleDeleteOperation( Database $dbForProject, @@ -473,6 +509,16 @@ class Update extends Action /** * Handle increment operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string $documentId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void + * @throws ConflictException + * @throws \Utopia\Database\Exception */ private function handleIncrementOperation( Database $dbForProject, @@ -510,6 +556,16 @@ class Update extends Action /** * Handle decrement operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param string $documentId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void + * @throws ConflictException + * @throws \Utopia\Database\Exception */ private function handleDecrementOperation( Database $dbForProject, @@ -547,6 +603,14 @@ class Update extends Action /** * Handle bulk create operation + * + * @param Database $dbForProject + * @param string $collectionId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void + * @throws \Utopia\Database\Exception */ private function handleBulkCreateOperation( Database $dbForProject, @@ -573,22 +637,33 @@ class Update extends Action /** * Handle bulk update operation with manual timestamp checking + * + * @param Database $dbForProject + * @param TransactionState $transactionState + * @param string $collectionId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws \Utopia\Database\Exception * @throws \Utopia\Database\Exception\Query * @throws ConflictException */ private function handleBulkUpdateOperation( Database $dbForProject, + TransactionState $transactionState, string $collectionId, array $data, \DateTime $createdAt, array &$state ): void { $queries = Query::parseQueries($data['queries'] ?? []); + $updateData = new Document($data['data']); + // First, update documents in the committed database $dbForProject->updateDocuments( $collectionId, - new Document($data['data']), + $updateData, $queries, onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt) { // Check if this document was created/modified in this transaction @@ -605,14 +680,27 @@ class Update extends Action $state[$collectionId][$updated->getId()] = $updated; } ); + + // Also update documents in the transaction state that match the query + $transactionState->applyBulkUpdateToState($collectionId, $updateData, $queries, $state); } /** * Handle bulk upsert operation with manual timestamp checking + * + * @param Database $dbForProject + * @param TransactionState $transactionState + * @param string $collectionId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws ConflictException + * @throws \Utopia\Database\Exception */ private function handleBulkUpsertOperation( Database $dbForProject, + TransactionState $transactionState, string $collectionId, array $data, \DateTime $createdAt, @@ -623,7 +711,11 @@ class Update extends Action return $doc instanceof Document ? $doc : new Document($doc); }, $data); - // Run bulk upsert without timestamp wrapper, checking manually in callback + // First, apply upserts to documents in the transaction state + // This ensures documents created in this transaction are updated properly + $transactionState->applyBulkUpsertToState($collectionId, $documents, $state); + + // Then run bulk upsert on committed database, checking manually in callback $dbForProject->upsertDocuments( $collectionId, $documents, @@ -649,11 +741,21 @@ class Update extends Action /** * Handle bulk delete operation with manual timestamp checking + * + * @param Database $dbForProject + * @param TransactionState $transactionState + * @param string $collectionId + * @param array $data + * @param \DateTime $createdAt + * @param array &$state + * @return void * @throws \Utopia\Database\Exception\Query * @throws ConflictException + * @throws \Utopia\Database\Exception */ private function handleBulkDeleteOperation( Database $dbForProject, + TransactionState $transactionState, string $collectionId, array $data, \DateTime $createdAt, @@ -681,5 +783,8 @@ class Update extends Action } } ); + + // Also delete documents in the transaction state that match the query + $transactionState->applyBulkDeleteToState($collectionId, $queries, $state); } } From 59ae403391892b33a4d67e1eeafcc18061ea1a8f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 17:34:34 +1300 Subject: [PATCH 131/145] Add more validation tests --- .../Http/TablesDB/Transactions/Update.php | 1 + .../Utopia/Database/Validator/Operation.php | 45 ++ .../Legacy/Transactions/TransactionsBase.php | 351 ++++++++++++++ .../Transactions/TransactionsBase.php | 449 ++++++++++++++++++ 4 files changed, 846 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index bcfb2db406..6bdf6df7ef 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -52,6 +52,7 @@ class Update extends TransactionsUpdate ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) ->inject('response') ->inject('dbForProject') + ->inject('transactionState') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('queueForStatsUsage') diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 2e3dc8e9e0..4e421689e3 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -159,6 +159,51 @@ class Operation extends Validator } } + // Bulk operation specific validations + $action = $value['action']; + + // BulkUpdate and BulkDelete require queries + if (\in_array($action, ['bulkUpdate', 'bulkDelete'])) { + if (!\array_key_exists('data', $value) || !\is_array($value['data'])) { + $this->description = "Key 'data' must be an array for {$action}"; + return false; + } + if (!\array_key_exists('queries', $value['data'])) { + $this->description = "Key 'queries' is required in data for {$action}"; + return false; + } + if (!\is_array($value['data']['queries'])) { + $this->description = "Key 'queries' must be an array for {$action}"; + return false; + } + } + + // BulkUpdate requires both queries and data + if ($action === 'bulkUpdate') { + if (!\array_key_exists('data', $value['data'])) { + $this->description = "Key 'data' is required in data for {$action}"; + return false; + } + if (!\is_array($value['data']['data'])) { + $this->description = "Key 'data.data' must be an array for {$action}"; + return false; + } + } + + // Increment and Decrement require specific keys + if (\in_array($action, ['increment', 'decrement'])) { + if (!\array_key_exists('data', $value) || !\is_array($value['data'])) { + $this->description = "Key 'data' must be an array for {$action}"; + return false; + } + // Get the attribute key name based on type + $attributeKey = $this->type === 'tablesdb' ? 'column' : 'attribute'; + if (!\array_key_exists($attributeKey, $value['data'])) { + $this->description = "Key '{$attributeKey}' is required in data for {$action}"; + return false; + } + } + return true; } diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php index f05563123c..a0485b5ef3 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php @@ -4225,4 +4225,355 @@ trait TransactionsBase sort($remainingIds); $this->assertEquals(['doc_6', 'doc_7', 'doc_8'], $remainingIds); } + + /** + * Test validation for invalid operation inputs + */ + public function testCreateOperationsValidation(): void + { + // Create database and collection for testing + $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' => 'ValidationTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $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' => 'ValidationTest', + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Add required attribute + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + + // Wait for attribute to be ready + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Invalid action type + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'invalidAction', + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 2: Missing required action field + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 3: Missing required databaseId field + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'collectionId' => $collectionId, + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4: Missing documentId for create operation + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 5: Missing data for create operation + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'documentId' => ID::unique() + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 6: BulkCreate with non-array data + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkCreate', + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'data' => 'not an array' + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 7: BulkUpdate with missing queries + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkUpdate', + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + 'data' => [ + 'data' => ['name' => 'Updated'] + ] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 8: Empty operations array + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 9: Operations not an array + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => 'not an array' + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test validation for committing/rolling back transactions + */ + public function testCommitRollbackValidation(): void + { + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Missing both commit and rollback + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 2: Both commit and rollback set to true + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true, + 'rollback' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 3: Invalid transaction ID + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/invalid_id", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Test 4: Attempt to commit already committed transaction + $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test validation for non-existent resources + */ + public function testNonExistentResources(): void + { + // Create database and transaction + $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' => 'ResourceTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Non-existent database + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => 'nonExistentDatabase', + 'collectionId' => 'someCollection', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Test 2: Non-existent collection + $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'collectionId' => 'nonExistentCollection', + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + } } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index 4e264e8641..2752e2d036 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -4225,4 +4225,453 @@ trait TransactionsBase sort($remainingIds); $this->assertEquals(['row_6', 'row_7', 'row_8'], $remainingIds); } + + /** + * Test validation for invalid operation inputs + */ + public function testCreateOperationsValidation(): void + { + // Create database and table for testing + $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' => 'ValidationTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $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' => 'ValidationTest', + 'rowSecurity' => false, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + // Add required column + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $column['headers']['status-code']); + + // Wait for column to be ready + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Invalid action type + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'invalidAction', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 2: Missing required action field + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 3: Missing required databaseId field + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'tableId' => $tableId, + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 4: Missing required tableId field + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 5: Missing rowId for create operation + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 6: Missing data for create operation + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'rowId' => ID::unique() + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 7: BulkCreate with non-array data + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkCreate', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'data' => 'not an array' + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 8: BulkUpdate with missing queries + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkUpdate', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'data' => [ + 'data' => ['name' => 'Updated'] + ] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 9: BulkUpdate with invalid query format + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkUpdate', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'data' => [ + 'queries' => 'not an array', + 'data' => ['name' => 'Updated'] + ] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 10: BulkDelete with missing queries + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'bulkDelete', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'data' => [] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 11: Increment with missing attribute + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'increment', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'rowId' => ID::unique(), + 'data' => ['value' => 1] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 12: Decrement with invalid value type + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'decrement', + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'rowId' => ID::unique(), + 'data' => [ + 'attribute' => 'counter', + 'value' => 'not a number' + ] + ] + ] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 13: Empty operations array + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [] + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 14: Operations not an array + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => 'not an array' + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test validation for committing/rolling back transactions + */ + public function testCommitRollbackValidation(): void + { + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Missing both commit and rollback + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 2: Both commit and rollback set to true + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true, + 'rollback' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // Test 3: Invalid transaction ID + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/invalid_id", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Commit the transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Test 4: Attempt to commit already committed transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + /** + * Test validation for non-existent resources + */ + public function testNonExistentResources(): void + { + // Create database and transaction + $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' => 'ResourceTestDatabase' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals(201, $transaction['headers']['status-code']); + $transactionId = $transaction['body']['$id']; + + // Test 1: Non-existent database + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => 'nonExistentDatabase', + 'tableId' => 'someTable', + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + // Test 2: Non-existent table + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'action' => 'create', + 'databaseId' => $databaseId, + 'tableId' => 'nonExistentTable', + 'rowId' => ID::unique(), + 'data' => ['name' => 'Test'] + ] + ] + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + } } From 2bc90db1f6f1593b48ab06bc9beaef4a71a5fd9a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 18:56:05 +1300 Subject: [PATCH 132/145] Fix tests --- src/Appwrite/Databases/TransactionState.php | 8 ++++---- .../Http/Databases/Transactions/Operations/Create.php | 4 ++++ .../Databases/Http/Databases/Transactions/Update.php | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 85ecb41ae2..e9cb6b82e4 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -574,25 +574,25 @@ class TransactionState } break; - case Query::TYPE_GREATER_THAN: + case Query::TYPE_GREATER: if (!($docValue > $values[0])) { return false; } break; - case Query::TYPE_GREATER_THAN_EQUAL: + case Query::TYPE_GREATER_EQUAL: if (!($docValue >= $values[0])) { return false; } break; - case Query::TYPE_LESSER_THAN: + case Query::TYPE_LESSER: if (!($docValue < $values[0])) { return false; } break; - case Query::TYPE_LESSER_THAN_EQUAL: + case Query::TYPE_LESSER_EQUAL: if (!($docValue <= $values[0])) { return false; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 20cf79551c..80122a6028 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -64,6 +64,10 @@ class Create extends Action public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, array $plan): void { + if (empty($operations)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); + } + $transaction = $dbForProject->getDocument('transactions', $transactionId); if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 19ba8f10d8..bd0c7f5582 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -129,7 +129,7 @@ class Update extends Action $totalOperations = 0; $databaseOperations = []; - $dbForProject->withTransaction(function () use ($dbForProject, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { + $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', ])); From deb3d7537539d958fe40c8ba0cbdd8e1f9b6044c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 3 Oct 2025 20:00:32 +1300 Subject: [PATCH 133/145] Explicit order asc --- src/Appwrite/Databases/TransactionState.php | 16 ++++++++++++++++ .../Http/Databases/Transactions/Update.php | 1 + 2 files changed, 17 insertions(+) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index e9cb6b82e4..31e09ccf75 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -4,6 +4,8 @@ namespace Appwrite\Databases; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception; +use Utopia\Database\Exception\Timeout; use Utopia\Database\Query; /** @@ -32,6 +34,9 @@ class TransactionState * @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, @@ -90,6 +95,9 @@ class TransactionState * @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, @@ -147,6 +155,9 @@ class TransactionState * @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, @@ -328,6 +339,9 @@ class TransactionState * * @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 { @@ -339,6 +353,8 @@ class TransactionState // Fetch operations ordered by sequence to replay in exact order $operations = $this->dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX) ]); $state = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index bd0c7f5582..525c74abc7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -137,6 +137,7 @@ class Update extends Action // Fetch operations ordered by sequence by default to replay operations in exact order they were created $operations = $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), Query::limit(PHP_INT_MAX), ]); From 89a47953d807643a8f66c233ba493688606f547e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 00:46:13 +1300 Subject: [PATCH 134/145] Fix tests --- src/Appwrite/Databases/TransactionState.php | 37 ++++--- .../Http/Databases/Transactions/Update.php | 96 ++++++++++--------- 2 files changed, 77 insertions(+), 56 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 31e09ccf75..645c25bf76 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -188,6 +188,8 @@ class TransactionState $adjustedCount = $baseCount; + $filters = $this->extractFilters($queries); + // Apply transaction state changes to the count foreach ($state[$collectionId] as $docId => $docState) { if (!$docState['exists']) { @@ -197,13 +199,13 @@ class TransactionState } } elseif ($docState['action'] === 'create') { // Document was created in transaction - if ($this->documentMatchesFilters($docState['document'], $queries)) { + if ($this->documentMatchesFilters($docState['document'], $filters)) { $adjustedCount++; // New document that matches } } elseif ($docState['action'] === 'update' || $docState['action'] === 'upsert') { // Document was updated/upserted $wasInResults = isset($committedDocIds[$docId]); - $nowMatches = $this->documentMatchesFilters($docState['document'], $queries); + $nowMatches = $this->documentMatchesFilters($docState['document'], $filters); if (!$wasInResults && $nowMatches && $docState['action'] === 'upsert') { $adjustedCount++; // Upsert created new document that matches @@ -259,8 +261,10 @@ class TransactionState return; } + $filters = $this->extractFilters($queries); + foreach ($state[$collectionId] as $docId => $doc) { - if ($this->documentMatchesFilters($doc, $queries)) { + if ($this->documentMatchesFilters($doc, $filters)) { // Apply the update to the state document foreach ($updateData->getArrayCopy() as $key => $value) { if ($key !== '$id') { @@ -290,8 +294,10 @@ class TransactionState return; } + $filters = $this->extractFilters($queries); + foreach ($state[$collectionId] as $docId => $doc) { - if ($this->documentMatchesFilters($doc, $queries)) { + if ($this->documentMatchesFilters($doc, $filters)) { unset($state[$collectionId][$docId]); } } @@ -501,19 +507,17 @@ class TransactionState } /** - * Check if a document matches filter queries + * Extract only filter queries from a query array * - * @param Document $doc Document to check - * @param array $queries Query filters - * @return bool True if document matches all filters + * @param array $queries Query array + * @return array Filtered queries */ - private function documentMatchesFilters(Document $doc, array $queries): bool + private function extractFilters(array $queries): array { - // Extract filter queries $filters = []; foreach ($queries as $query) { $method = $query->getMethod(); - // Only process filter queries, not limit/offset/cursor/select + // Only process filter queries, not limit/offset/cursor/select/order if (!\in_array($method, [ Query::TYPE_LIMIT, Query::TYPE_OFFSET, @@ -526,7 +530,18 @@ class TransactionState $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 no filters, document matches if (empty($filters)) { return true; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 525c74abc7..c17fb7e75f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -129,22 +129,22 @@ class Update extends Action $totalOperations = 0; $databaseOperations = []; - $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'committing', - ])); + try { + $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'committing', + ])); - // Fetch operations ordered by sequence by default to replay operations in exact order they were created - $operations = $dbForProject->find('transactionLogs', [ - Query::equal('transactionInternalId', [$transaction->getSequence()]), - Query::orderAsc(), - Query::limit(PHP_INT_MAX), - ]); + // Fetch operations ordered by sequence by default to replay operations in exact order they were created + $operations = $dbForProject->find('transactionLogs', [ + Query::equal('transactionInternalId', [$transaction->getSequence()]), + Query::orderAsc(), + Query::limit(PHP_INT_MAX), + ]); - // Track transaction state for cross-operation visibility - $state = []; + // Track transaction state for cross-operation visibility + $state = []; - try { foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; $collectionInternalId = $operation['collectionInternalId']; @@ -207,38 +207,44 @@ class Update extends Action $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); - } catch (NotFoundException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); - } catch (DuplicateException|ConflictException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); - } catch (StructureException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } catch (LimitException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); - } catch (TransactionException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); - } catch (QueryException $e) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ - 'status' => 'failed', - ])); - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - }); + }); + } catch (NotFoundException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); + } catch (DuplicateException|ConflictException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); + } catch (StructureException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } catch (LimitException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); + } catch (TransactionException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); + } catch (QueryException $e) { + // Transaction has been rolled back, now mark it as failed + $dbForProject->updateDocument('transactions', $transactionId, new Document([ + 'status' => 'failed', + ])); + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } $queueForStatsUsage ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations); From e93e03c248619e2153bd3761f1746f6d4f56b0c7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 01:15:53 +1300 Subject: [PATCH 135/145] Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Modules/Databases/Http/Databases/Transactions/Update.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 525c74abc7..a1f6bd2a65 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -591,8 +591,9 @@ class Update extends Action } // Use timestamp wrapper for independent operations - $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data) { - $dbForProject->decreaseDocumentAttribute( + // Use timestamp wrapper for independent operations + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { + $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( collection: $collectionId, id: $documentId, attribute: $data[$this->getAttributeKey()], From 2c7cf7826b59205910525df1febae6fcb09c5606 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 16:43:39 +1300 Subject: [PATCH 136/145] Fix operation perm checks --- .../Http/Databases/Transactions/Create.php | 5 +- .../Transactions/Operations/Create.php | 103 ++- .../Http/Databases/Transactions/Update.php | 148 ++-- .../Http/TablesDB/Transactions/Update.php | 1 + .../Legacy/Transactions/TransactionsBase.php | 117 +-- .../TablesDB/Transactions/PermissionsBase.php | 670 ++++++++++++++++++ .../PermissionsCustomClientTest.php | 14 + .../Transactions/TransactionsBase.php | 117 +-- 8 files changed, 962 insertions(+), 213 deletions(-) create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php create mode 100644 tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsCustomClientTest.php diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index a5866ba7d1..e67c1d08d0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Swoole\Response as SwooleResponse; @@ -57,12 +58,12 @@ class Create extends Action public function action(int $ttl, UtopiaResponse $response, Database $dbForProject): void { - $transaction = $dbForProject->createDocument('transactions', new Document([ + $transaction = Authorization::skip(fn () => $dbForProject->createDocument('transactions', new Document([ '$id' => ID::unique(), 'status' => 'pending', 'operations' => 0, 'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl), - ])); + ]))); $response ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 80122a6028..f7314fb87c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -14,6 +14,8 @@ use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; @@ -68,7 +70,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); } - $transaction = $dbForProject->getDocument('transactions', $transactionId); + $transaction = Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)); if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction'); } @@ -97,18 +99,103 @@ class Create extends Action throw new Exception(Exception::USER_UNAUTHORIZED); } - $database = $databases[$operation['databaseId']] ??= $dbForProject->getDocument('databases', $operation['databaseId']); - if ($database->isEmpty()) { + $database = $databases[$operation['databaseId']] ??= Authorization::skip(fn () => $dbForProject->getDocument('databases', $operation['databaseId'])); + if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = $collections[$operation[$this->getGroupId()]] ??= - $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()]); + Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $operation[$this->getGroupId()])); - if ($collection->isEmpty()) { + if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } + // Check if collection has relationships for bulk operations + if (\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) { + $hasRelationships = \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + ); + if ($hasRelationships) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk operations are not supported for ' . $this->getGroupId() . ' with relationship attributes'); + } + } + + // For update, upsert, delete, check document existence first + $document = null; + if (\in_array($operation['action'], ['update', 'delete', 'upsert'])) { + $documentId = $operation[$this->getResourceId()] ?? null; + if (empty($documentId)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Document ID is required for ' . $operation['action'] . ' operations'); + } + + $document = Authorization::skip(fn () => $dbForProject->getDocument( + 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + $documentId + )); + + if ($document->isEmpty() && \in_array($operation['action'], ['update', 'delete'])) { + throw new Exception(Exception::DOCUMENT_NOT_FOUND); + } + } + + $permissionType = match ($operation['action']) { + 'create', 'bulkCreate' => Database::PERMISSION_CREATE, + 'update', 'bulkUpdate' => Database::PERMISSION_UPDATE, + 'delete', 'bulkDelete' => Database::PERMISSION_DELETE, + 'upsert', 'bulkUpsert' => ($document && !$document->isEmpty()) ? Database::PERMISSION_UPDATE : Database::PERMISSION_CREATE, + default => throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid action: ' . $operation['action']) + }; + + if (!$isAPIKey && !$isPrivilegedUser) { + $documentSecurity = $collection->getAttribute('documentSecurity', false); + $validator = new Authorization($permissionType); + $collectionValid = $validator->isValid($collection->getPermissionsByType($permissionType)); + $documentValid = false; + if ($document !== null && !$document->isEmpty() && $documentSecurity) { + if ($permissionType === Database::PERMISSION_UPDATE) { + $documentValid = $validator->isValid($document->getUpdate()); + } elseif ($permissionType === Database::PERMISSION_DELETE) { + $documentValid = $validator->isValid($document->getDelete()); + } + } + + if ($permissionType === Database::PERMISSION_CREATE || !$documentSecurity) { + if (!$collectionValid) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + } else { + if (!$collectionValid && !$documentValid) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + } + + // Users can only set permissions for roles they have + if (isset($operation['data']['$permissions'])) { + $permissions = $operation['data']['$permissions'] ?? null; + if (!\is_null($permissions)) { + $roles = Authorization::getRoles(); + foreach (Database::PERMISSIONS as $type) { + foreach ($permissions as $permission) { + $permission = Permission::parse($permission); + if ($permission->getPermission() != $type) { + continue; + } + $role = (new Role( + $permission->getRole(), + $permission->getIdentifier(), + $permission->getDimension() + ))->toString(); + if (!Authorization::isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); + } + } + } + } + } + } + $staged[] = new Document([ '$id' => ID::unique(), 'databaseInternalId' => $database->getSequence(), @@ -121,13 +208,13 @@ class Create extends Action } $transaction = $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { - $dbForProject->createDocuments('transactionLogs', $staged); - return $dbForProject->increaseDocumentAttribute( + Authorization::skip(fn () => $dbForProject->createDocuments('transactionLogs', $staged)); + return Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( 'transactions', $transactionId, 'operations', \count($operations) - ); + )); }); $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index c17fb7e75f..5c949c9227 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -14,9 +14,10 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Authorization; +use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Query as QueryException; @@ -67,6 +68,7 @@ class Update extends Action ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) ->inject('response') ->inject('dbForProject') + ->inject('user') ->inject('transactionState') ->inject('queueForDeletes') ->inject('queueForEvents') @@ -83,6 +85,7 @@ class Update extends Action * @param bool $rollback * @param UtopiaResponse $response * @param Database $dbForProject + * @param Document $user * @param TransactionState $transactionState * @param Delete $queueForDeletes * @param Event $queueForEvents @@ -99,8 +102,9 @@ class Update extends Action * @throws Structure * @throws \Utopia\Exception */ - public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void + public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { + if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); } @@ -108,7 +112,7 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); } - $transaction = $dbForProject->getDocument('transactions', $transactionId); + $transaction = Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)); if ($transaction->isEmpty()) { throw new Exception(Exception::TRANSACTION_NOT_FOUND); } @@ -123,6 +127,7 @@ class Update extends Action } if ($commit) { + $operations = []; // Track metrics for usage stats @@ -131,16 +136,17 @@ class Update extends Action try { $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks) { - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'committing', - ])); + ]))); // Fetch operations ordered by sequence by default to replay operations in exact order they were created - $operations = $dbForProject->find('transactionLogs', [ + $operations = Authorization::skip(fn () => $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::orderAsc(), Query::limit(PHP_INT_MAX), - ]); + ])); + // Track transaction state for cross-operation visibility $state = []; @@ -154,6 +160,15 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; + // For delete operations, fetch the document before deleting for realtime events + if ($action === 'delete' && $documentId && empty($data)) { + $doc = $dbForProject->getDocument($collectionId, $documentId); + if (!$doc->isEmpty()) { + $operation['data'] = $doc->getArrayCopy(); + $data = $operation['data']; + } + } + // Track operations for stats $totalOperations++; $databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1; @@ -197,52 +212,53 @@ class Update extends Action } } - $transaction = $dbForProject->updateDocument( + $transaction = Authorization::skip(fn () => $dbForProject->updateDocument( 'transactions', $transactionId, new Document(['status' => 'committed']) - ); + )); // Clear the transaction logs $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); }); + } catch (NotFoundException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); } catch (DuplicateException|ConflictException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); } catch (StructureException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (LimitException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); } catch (TransactionException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } catch (QueryException $e) { // Transaction has been rolled back, now mark it as failed - $dbForProject->updateDocument('transactions', $transactionId, new Document([ + Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', - ])); + ]))); throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } @@ -258,23 +274,27 @@ class Update extends Action } // Trigger realtime events for each operation + foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; $collectionInternalId = $operation['collectionInternalId']; + $collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}"; $action = $operation['action']; $documentId = $operation['documentId']; $data = $operation['data']; + if ($data instanceof Document) { $data = $data->getArrayCopy(); } - $database = $dbForProject->findOne('databases', [ + $database = Authorization::skip(fn () => $dbForProject->findOne('databases', [ Query::equal('$sequence', [$databaseInternalId]) - ]); - $collection = $dbForProject->findOne('database_' . $databaseInternalId, [ + ])); + + $collection = Authorization::skip(fn () => $dbForProject->findOne('database_' . $databaseInternalId, [ Query::equal('$sequence', [$collectionInternalId]) - ]); + ])); $groupId = $this->getGroupId(); $resourceId = $this->getResourceId(); @@ -290,26 +310,49 @@ class Update extends Action ->setContext($contextKey, $collection); $eventAction = ''; - $documents = []; + $documentsToTrigger = []; switch ($action) { case 'create': $eventAction = 'create'; - $documents[] = $documentId ?? $data['$id'] ?? null; + $docId = $documentId ?? $data['$id'] ?? null; + if ($docId) { + // Fetch the created document from the database + $doc = $dbForProject->getDocument($collectionId, $docId); + if (!$doc->isEmpty()) { + $documentsToTrigger[] = $doc; + } + } break; case 'update': case 'increment': case 'decrement': $eventAction = 'update'; - $documents[] = $documentId; + if ($documentId) { + // Fetch the updated document from the database + $doc = $dbForProject->getDocument($collectionId, $documentId); + if (!$doc->isEmpty()) { + $documentsToTrigger[] = $doc; + } + } break; case 'delete': $eventAction = 'delete'; - $documents[] = $documentId; + if ($documentId && !empty($data)) { + // For delete, use the fetched document data (fetched before deletion) + $documentsToTrigger[] = new Document(array_merge($data, ['$id' => $documentId])); + } break; case 'upsert': - $eventAction = 'upsert'; - $documents[] = $documentId ?? $data['$id'] ?? null; + $eventAction = 'update'; // Upsert is treated as update for events + $docId = $documentId ?? $data['$id'] ?? null; + if ($docId) { + // Fetch the upserted document from the database + $doc = $dbForProject->getDocument($collectionId, $docId); + if (!$doc->isEmpty()) { + $documentsToTrigger[] = $doc; + } + } break; case 'bulkCreate': case 'bulkUpdate': @@ -319,32 +362,43 @@ class Update extends Action } // Trigger events for each document - foreach ($documents as $docId) { - if ($docId) { - $queueForEvents - ->setParam('documentId', $docId) - ->setParam('rowId', $docId) - ->setEvent("databases.[databaseId].{$contextKey}s.[{$groupId}].{$resourcePlural}.[{$resourceId}]." . $eventAction); - $queueForRealtime->from($queueForEvents)->trigger(); - $queueForFunctions->from($queueForEvents)->trigger(); - $queueForWebhooks->from($queueForEvents)->trigger(); + $eventString = "databases.[databaseId].{$contextKey}s.[{$groupId}].{$resourcePlural}.[{$resourceId}]." . $eventAction; - $queueForEvents->reset(); - $queueForRealtime->reset(); - $queueForFunctions->reset(); - $queueForWebhooks->reset(); - } + $queueForEvents->setEvent($eventString); + + foreach ($documentsToTrigger as $doc) { + + // Add table/collection IDs to the payload for realtime channels + $payload = $doc->getArrayCopy(); + $payload['$tableId'] = $collection->getId(); + $payload['$collectionId'] = $collection->getId(); + + $queueForEvents + ->setParam('documentId', $doc->getId()) + ->setParam('rowId', $doc->getId()) + ->setPayload($payload); + + $project = $queueForEvents->getProject(); + $result = $queueForRealtime->from($queueForEvents)->trigger(); + + $queueForFunctions->from($queueForEvents)->trigger(); + $queueForWebhooks->from($queueForEvents)->trigger(); } + + $queueForEvents->reset(); + $queueForRealtime->reset(); + $queueForFunctions->reset(); + $queueForWebhooks->reset(); } } if ($rollback) { - $transaction = $dbForProject->updateDocument( + $transaction = Authorization::skip(fn () => $dbForProject->updateDocument( 'transactions', $transactionId, new Document(['status' => 'failed']) - ); + )); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php index 6bdf6df7ef..72a6a9da6f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -52,6 +52,7 @@ class Update extends TransactionsUpdate ->param('rollback', false, new Boolean(), 'Rollback transaction?', true) ->inject('response') ->inject('dbForProject') + ->inject('user') ->inject('transactionState') ->inject('queueForDeletes') ->inject('queueForEvents') diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php index a0485b5ef3..7c84295a4f 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php @@ -32,8 +32,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); @@ -49,8 +48,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 900 ]); @@ -69,8 +67,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 30 // Below minimum ]); @@ -79,8 +76,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 4000 // Above maximum ]); @@ -109,8 +105,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -299,8 +294,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -410,8 +404,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -542,8 +535,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 60 ]); @@ -633,8 +625,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -769,14 +760,12 @@ trait TransactionsBase $txn1 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $txn2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId1 = $txn1['body']['$id']; $transactionId2 = $txn2['body']['$id']; @@ -908,8 +897,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1027,8 +1015,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1190,8 +1177,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1302,8 +1288,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1350,8 +1335,7 @@ trait TransactionsBase $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; @@ -1428,8 +1412,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1467,8 +1450,7 @@ trait TransactionsBase $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; @@ -1564,8 +1546,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -1703,8 +1684,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1815,8 +1795,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1944,8 +1923,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2047,8 +2025,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2207,8 +2184,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2331,8 +2307,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2479,8 +2454,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2616,8 +2590,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; $this->assertEquals(201, $transaction['headers']['status-code']); @@ -2860,8 +2833,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3037,8 +3009,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3219,8 +3190,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3384,8 +3354,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3547,8 +3516,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3704,8 +3672,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3861,8 +3828,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4012,8 +3978,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4163,8 +4128,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4283,8 +4247,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -4450,8 +4413,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -4532,8 +4494,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php new file mode 100644 index 0000000000..41487b2bb8 --- /dev/null +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php @@ -0,0 +1,670 @@ +client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'PermissionsTestDB' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $this->permissionsDatabase = $database['body']['$id']; + } + + /** + * Test collection-level create permission check on staging + */ + public function testCollectionCreatePermissionDenied(): void + { + // Create a collection with no create permission for current user + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest1', + 'name' => 'Permission Test 1', + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to stage a create operation without permission, should fail + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'create', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc1', + 'data' => ['title' => 'Test Document'], + ]] + ]); + + // This should fail with 401 Unauthorized + if ($staged['headers']['status-code'] !== 401) { + echo "\nDEBUG - Actual response code: " . $staged['headers']['status-code'] . "\n"; + echo "DEBUG - Response body: " . json_encode($staged['body'], JSON_PRETTY_PRINT) . "\n"; + } + $this->assertEquals(401, $staged['headers']['status-code']); + } + + /** + * Test collection-level update permission check on staging + */ + public function testCollectionUpdatePermissionDenied(): void + { + // Create a collection with create but no update permission + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest2', + 'name' => 'Permission Test 2', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create a document first with API key + $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'testDoc2', + 'data' => ['title' => 'Original Title'], + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to stage an update operation without permission, should fail + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'update', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc2', + 'data' => ['title' => 'Updated Title'], + ]] + ]); + + // This should fail with 401 Unauthorized + $this->assertEquals(401, $staged['headers']['status-code']); + } + + /** + * Test collection-level delete permission check on staging + */ + public function testCollectionDeletePermissionDenied(): void + { + // Create a collection with create, read but no delete permission + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest3', + 'name' => 'Permission Test 3', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'testDoc3', + 'data' => ['title' => 'To Be Deleted'], + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to stage a delete operation without permission, should fail + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'delete', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc3', + 'data' => [], + ]] + ]); + + // This should fail with 401 Unauthorized + $this->assertEquals(401, $staged['headers']['status-code']); + } + + /** + * Test document-level update permission grants access when rowSecurity is enabled + * Collection has no update permission, but document does, should succeed + */ + public function testDocumentLevelUpdatePermissionGranted(): void + { + // Create collection with rowSecurity enabled but no update permission at collection level + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest4', + 'name' => 'Permission Test 4', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create a document with update permission at document level + $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'testDoc4', + 'data' => ['title' => 'Protected Document'], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Stage an update, should succeed because document has update permission + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'update', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc4', + 'data' => ['title' => 'Trying to Update'], + ]] + ]); + + // This should succeed with 201 because document has update permission + $this->assertEquals(201, $staged['headers']['status-code']); + } + + /** + * Test document-level delete permission grants access when rowSecurity is enabled + * Collection has no delete permission, but document does, should succeed + */ + public function testDocumentLevelDeletePermissionGranted(): void + { + // Create collection with rowSecurity enabled but no delete permission at collection level + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest5', + 'name' => 'Permission Test 5', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create a document with delete permission at document level + $doc = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'testDoc5', + 'data' => ['title' => 'Can Delete Me'], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $doc['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Stage a delete should succeed because document has delete permission + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'delete', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc5', + 'data' => [], + ]] + ]); + + // This should succeed with 201 because document has DELETE permission + $this->assertEquals(201, $staged['headers']['status-code']); + } + + /** + * Test that users cannot set permissions for roles they don't have + */ + public function testCannotSetUnauthorizedRolePermissions(): void + { + // Create a collection + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest6', + 'name' => 'Permission Test 6', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + // Add attribute + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to stage a create with team permissions, current user is not in team + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'create', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc6', + 'data' => [ + 'title' => 'Admin Only Doc', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::team('adminTeam')), + ], + ], + ]] + ]); + + // This should fail with 401 Unauthorized, cannot set permissions for roles you don't have + $this->assertEquals(401, $staged['headers']['status-code']); + $this->assertStringContainsString('Permissions must be one of', $staged['body']['message']); + } + + /** + * Test successful staging when user has the required permissions + */ + public function testSuccessfulStagingWithProperPermissions(): void + { + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest7', + 'name' => 'Permission Test 7', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => true, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Stage a create with permissions for current user's roles, should succeed + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'create', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'testDoc7', + 'data' => [ + 'title' => 'Valid Document', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($this->getUser()['$id'])), + ], + ], + ]] + ]); + + // This should succeed + $this->assertEquals(201, $staged['headers']['status-code']); + $this->assertEquals(1, $staged['body']['operations']); + } + + /** + * Test that non-existent documents cannot be updated in transactions + */ + public function testCannotUpdateNonExistentDocument(): void + { + // Create a collection + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest8', + 'name' => 'Permission Test 8', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to update a document that doesn't exist - should fail + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'update', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'nonExistentDoc', + 'data' => ['title' => 'Trying to Update'], + ]] + ]); + + // This should fail with 404 Not Found + $this->assertEquals(404, $staged['headers']['status-code']); + } + + /** + * Test that non-existent documents cannot be deleted in transactions + */ + public function testCannotDeleteNonExistentDocument(): void + { + // Create a collection + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest9', + 'name' => 'Permission Test 9', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Try to delete a document that doesn't exist, should fail + $staged = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'delete', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'nonExistentDoc', + 'data' => [], + ]] + ]); + + // This should fail with 404 Not Found + $this->assertEquals(404, $staged['headers']['status-code']); + } +} diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsCustomClientTest.php b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsCustomClientTest.php new file mode 100644 index 0000000000..fa33aea7b6 --- /dev/null +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsCustomClientTest.php @@ -0,0 +1,14 @@ +client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); @@ -49,8 +48,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 900 ]); @@ -69,8 +67,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 30 // Below minimum ]); @@ -79,8 +76,7 @@ trait TransactionsBase $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 4000 // Above maximum ]); @@ -109,8 +105,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -299,8 +294,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -410,8 +404,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -542,8 +535,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ + ], $this->getHeaders()), [ 'ttl' => 60 ]); @@ -633,8 +625,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -769,14 +760,12 @@ trait TransactionsBase $txn1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $txn2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId1 = $txn1['body']['$id']; $transactionId2 = $txn2['body']['$id']; @@ -908,8 +897,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1027,8 +1015,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1190,8 +1177,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1302,8 +1288,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1350,8 +1335,7 @@ trait TransactionsBase $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; @@ -1428,8 +1412,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1467,8 +1450,7 @@ trait TransactionsBase $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; @@ -1564,8 +1546,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -1703,8 +1684,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1815,8 +1795,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -1944,8 +1923,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2047,8 +2025,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2207,8 +2184,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2331,8 +2307,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2479,8 +2454,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -2616,8 +2590,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; $this->assertEquals(201, $transaction['headers']['status-code']); @@ -2860,8 +2833,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3037,8 +3009,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3219,8 +3190,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3384,8 +3354,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3547,8 +3516,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3704,8 +3672,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -3861,8 +3828,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4012,8 +3978,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4163,8 +4128,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; @@ -4283,8 +4247,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -4548,8 +4511,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; @@ -4630,8 +4592,7 @@ trait TransactionsBase $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); + ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; From 9edc0c16d8cb5854679e0b92a9ee7f9f4034c33b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 16:48:44 +1300 Subject: [PATCH 137/145] Realtime txn tests --- .../Realtime/RealtimeCustomClientTest.php | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 3e57c5e9bc..4e7a2a398a 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -2507,4 +2507,462 @@ class RealtimeCustomClientTest extends Scope $client->close(); } + + public function testChannelDatabaseTransaction() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['documents'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertEquals('connected', $response['type']); + + /** + * Setup 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' => 'Transactions DB', + ]); + + $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' => 'Test Collection', + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + 'documentSecurity' => true, + ]); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + /** + * Test Transaction Create with Single Document + */ + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 3600 // 1 hour + ]); + + $this->assertEquals(201, $transaction['headers']['status-code'], 'Failed to create transaction: ' . json_encode($transaction['body'])); + $this->assertNotEmpty($transaction['body']['$id']); + + $transactionId = $transaction['body']['$id']; + $documentId = ID::unique(); + + $operationsResponse = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId, + 'action' => 'create', + 'data' => [ + 'name' => 'Transaction Document', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ], + ] + ] + ]); + + $this->assertEquals(201, $operationsResponse['headers']['status-code'], 'Failed to add operations: ' . json_encode($operationsResponse['body'])); + + // Commit transaction + $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'], 'Failed to commit transaction: ' . json_encode($commitResponse['body'])); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('event', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertContains('documents', $response['data']['channels']); + $this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows.{$documentId}.create", $response['data']['events']); + $this->assertNotEmpty($response['data']['payload']); + $this->assertEquals('Transaction Document', $response['data']['payload']['name']); + + /** + * Test Transaction Update + */ + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 3600 + ]); + + $transactionId = $transaction['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId, + 'action' => 'update', + 'data' => [ + 'name' => 'Updated Transaction Document', + ], + ] + ] + ]); + + $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 + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertEquals('event', $response['type']); + $this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows.{$documentId}.update", $response['data']['events']); + $this->assertEquals('Updated Transaction Document', $response['data']['payload']['name']); + + /** + * Test Transaction Delete + */ + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 3600 + ]); + + $transactionId = $transaction['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId, + 'action' => 'delete', + ] + ] + ]); + + $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 + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertEquals('event', $response['type']); + $this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows.{$documentId}.delete", $response['data']['events']); + + $client->close(); + } + + public function testChannelDatabaseTransactionMultipleOperations() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['documents'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client->receive(), true); + $this->assertEquals('connected', $response['type']); + + /** + * Setup 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' => 'Multi-Op DB', + ]); + + $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' => 'Test Collection', + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + 'documentSecurity' => true, + ]); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + /** + * Test Multiple Operations in Single Transaction + */ + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 3600 + ]); + + $transactionId = $transaction['body']['$id']; + $documentId1 = ID::unique(); + $documentId2 = ID::unique(); + $documentId3 = ID::unique(); + + $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId1, + 'action' => 'create', + 'data' => [ + 'name' => 'Doc 1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ], + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId2, + 'action' => 'create', + 'data' => [ + 'name' => 'Doc 2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ], + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId3, + 'action' => 'create', + 'data' => [ + 'name' => 'Doc 3', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ], + ] + ] + ]); + + $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 + ]); + + // Should receive 3 events, one for each document + $response1 = json_decode($client->receive(), true); + $response2 = json_decode($client->receive(), true); + $response3 = json_decode($client->receive(), true); + + $this->assertEquals('event', $response1['type']); + $this->assertEquals('event', $response2['type']); + $this->assertEquals('event', $response3['type']); + + $receivedDocIds = [ + $response1['data']['payload']['$id'], + $response2['data']['payload']['$id'], + $response3['data']['payload']['$id'], + ]; + + $this->assertContains($documentId1, $receivedDocIds); + $this->assertContains($documentId2, $receivedDocIds); + $this->assertContains($documentId3, $receivedDocIds); + + $client->close(); + } + + public function testChannelDatabaseTransactionRollback() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['documents'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client->receive(), true); + $this->assertEquals('connected', $response['type']); + + /** + * Setup 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' => 'Rollback DB', + ]); + + $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' => 'Test Collection', + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + 'documentSecurity' => true, + ]); + + $collectionId = $collection['body']['$id']; + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + sleep(2); + + /** + * Test Transaction Rollback - Should NOT trigger realtime events + */ + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'ttl' => 3600 + ]); + + $transactionId = $transaction['body']['$id']; + $documentId = ID::unique(); + + $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $collectionId, + 'rowId' => $documentId, + 'action' => 'create', + 'data' => ['name' => 'Rollback Document'], + ] + ] + ]); + + // Rollback transaction + $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transactionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rollback' => true + ]); + + // Wait a bit to ensure no event is received + sleep(1); + + try { + $client->receive(1); // 1 second timeout + $this->fail('Should not receive any event after rollback'); + } catch (TimeoutException $e) { + // Expected - no event should be triggered + $this->assertTrue(true); + } + + $client->close(); + } } From 3e77810e13d2ec90b78627860b39cf0af292e6dd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 19:26:22 +1300 Subject: [PATCH 138/145] Fix existence check for tracked + staged ops --- src/Appwrite/Databases/TransactionState.php | 11 +- .../Http/Databases/Transactions/Create.php | 2 +- .../Transactions/Operations/Create.php | 72 ++++++----- .../Http/Databases/Transactions/Update.php | 3 +- .../Transactions/Operations/Create.php | 1 + .../TablesDB/Transactions/PermissionsBase.php | 113 ++++++++++++++++++ .../Realtime/RealtimeCustomClientTest.php | 7 +- 7 files changed, 167 insertions(+), 42 deletions(-) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 645c25bf76..2d35ce92ae 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -7,6 +7,7 @@ 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 @@ -351,17 +352,17 @@ class TransactionState */ private function getTransactionState(string $transactionId): array { - $transaction = $this->dbForProject->getDocument('transactions', $transactionId); + $transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId)); if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') { return []; } // Fetch operations ordered by sequence to replay in exact order - $operations = $this->dbForProject->find('transactionLogs', [ + $operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::orderAsc(), Query::limit(PHP_INT_MAX) - ]); + ])); $state = []; @@ -381,6 +382,10 @@ class TransactionState case 'create': $docId = $documentId ?? ($data['$id'] ?? null); if ($docId) { + // Ensure document has the correct $id + if (!isset($data['$id'])) { + $data['$id'] = $docId; + } $state[$collectionId][$docId] = [ 'action' => 'create', 'document' => new Document($data), diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php index e67c1d08d0..744ad33540 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Create.php @@ -9,9 +9,9 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\DateTime; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\Range; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index f7314fb87c..5f6fa4f1a6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations; use Appwrite\Auth\Auth; +use Appwrite\Databases\TransactionState; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action; use Appwrite\SDK\AuthType; @@ -60,11 +61,12 @@ class Create extends Action ->param('operations', [], new ArrayList(new Operation(type: 'legacy')), 'Array of staged operations.', true) ->inject('response') ->inject('dbForProject') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } - public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, array $plan): void + public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan): void { if (empty($operations)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); @@ -88,7 +90,7 @@ class Create extends Action $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $databases = $collections = $staged = []; + $databases = $collections = $staged = $dependants = []; foreach ($operations as $operation) { if (!$isAPIKey && !$isPrivilegedUser && \in_array($operation['action'], [ 'bulkCreate', @@ -122,27 +124,26 @@ class Create extends Action } } - // For update, upsert, delete, check document existence first + // For update, upsert, delete, increment, decrement, check document existence first $document = null; - if (\in_array($operation['action'], ['update', 'delete', 'upsert'])) { + if (\in_array($operation['action'], ['update', 'delete', 'upsert', 'increment', 'decrement'])) { $documentId = $operation[$this->getResourceId()] ?? null; if (empty($documentId)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Document ID is required for ' . $operation['action'] . ' operations'); } - $document = Authorization::skip(fn () => $dbForProject->getDocument( - 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), - $documentId - )); + $collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + $isDependant = isset($dependants[$collectionKey][$documentId]); - if ($document->isEmpty() && \in_array($operation['action'], ['update', 'delete'])) { + $document = $transactionState->getDocument($collectionKey, $documentId, $transactionId); + if ($document->isEmpty() && !$isDependant && $operation['action'] !== 'upsert') { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } } $permissionType = match ($operation['action']) { 'create', 'bulkCreate' => Database::PERMISSION_CREATE, - 'update', 'bulkUpdate' => Database::PERMISSION_UPDATE, + 'update', 'bulkUpdate', 'increment', 'decrement' => Database::PERMISSION_UPDATE, 'delete', 'bulkDelete' => Database::PERMISSION_DELETE, 'upsert', 'bulkUpsert' => ($document && !$document->isEmpty()) ? Database::PERMISSION_UPDATE : Database::PERMISSION_CREATE, default => throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid action: ' . $operation['action']) @@ -173,23 +174,21 @@ class Create extends Action // Users can only set permissions for roles they have if (isset($operation['data']['$permissions'])) { - $permissions = $operation['data']['$permissions'] ?? null; - if (!\is_null($permissions)) { - $roles = Authorization::getRoles(); - foreach (Database::PERMISSIONS as $type) { - foreach ($permissions as $permission) { - $permission = Permission::parse($permission); - if ($permission->getPermission() != $type) { - continue; - } - $role = (new Role( - $permission->getRole(), - $permission->getIdentifier(), - $permission->getDimension() - ))->toString(); - if (!Authorization::isRole($role)) { - throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); - } + $permissions = $operation['data']['$permissions']; + $roles = Authorization::getRoles(); + foreach (Database::PERMISSIONS as $type) { + foreach ($permissions as $permission) { + $permission = Permission::parse($permission); + if ($permission->getPermission() != $type) { + continue; + } + $role = (new Role( + $permission->getRole(), + $permission->getIdentifier(), + $permission->getDimension() + ))->toString(); + if (!Authorization::isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); } } } @@ -205,17 +204,26 @@ class Create extends Action 'action' => $operation['action'], 'data' => $operation['data'] ?? [], ]); + + // Track create operations for dependent update/increment/decrement/delete operations in same batch + if ($operation['action'] === 'create') { + $collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + $documentId = $operation[$this->getResourceId()] ?? null; + if ($documentId) { + $dependants[$collectionKey][$documentId] = true; + } + } } - $transaction = $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { - Authorization::skip(fn () => $dbForProject->createDocuments('transactionLogs', $staged)); - return Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( + $transaction = Authorization::skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) { + $dbForProject->createDocuments('transactionLogs', $staged); + return $dbForProject->increaseDocumentAttribute( 'transactions', $transactionId, 'operations', \count($operations) - )); - }); + ); + })); $response ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 0d7ca5c252..d522c699a8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -14,16 +14,15 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Swoole\Response as SwooleResponse; use Utopia\Validator\Boolean; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php index eab0fed161..d85020b2bc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Operations/Create.php @@ -52,6 +52,7 @@ class Create extends OperationsCreate ->param('operations', [], new ArrayList(new Operation(type: 'tablesdb')), 'Array of staged operations.', true) ->inject('response') ->inject('dbForProject') + ->inject('transactionState') ->inject('plan') ->callback($this->action(...)); } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php index 41487b2bb8..9dac59e2ec 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/PermissionsBase.php @@ -667,4 +667,117 @@ trait PermissionsBase // This should fail with 404 Not Found $this->assertEquals(404, $staged['headers']['status-code']); } + + /** + * Test that a document created in one batch can be updated in a subsequent batch within the same transaction + * This validates the transactionState->getDocument() fix for cross-batch dependencies + */ + public function testCanUpdateDocumentCreatedInPreviousBatch(): void + { + $collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => 'permTest10', + 'name' => 'Permission Test 10', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rowSecurity' => false, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(201, $transaction['headers']['status-code']); + + // Batch 1: Create a document + $batch1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'create', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'crossBatchDoc', + 'data' => [ + 'title' => 'Initial Title', + ], + ]] + ]); + + $this->assertEquals(201, $batch1['headers']['status-code']); + $this->assertEquals(1, $batch1['body']['operations']); + + // Batch 2: Update the document created in batch 1 + $batch2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'update', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'crossBatchDoc', + 'data' => [ + 'title' => 'Updated Title', + ], + ]] + ]); + + // This should succeed with 201 because transactionState finds the staged document from batch 1 + $this->assertEquals(201, $batch2['headers']['status-code']); + $this->assertEquals(2, $batch2['body']['operations']); + + // Batch 3: Delete the same document + $batch3 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transaction['body']['$id'] . '/operations', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'operations' => [[ + 'action' => 'delete', + 'databaseId' => $this->permissionsDatabase, + 'tableId' => $collection['body']['$id'], + 'rowId' => 'crossBatchDoc', + 'data' => [], + ]] + ]); + + // This should also succeed with 201 + $this->assertEquals(201, $batch3['headers']['status-code']); + $this->assertEquals(3, $batch3['body']['operations']); + + // Rollback to clean up + $rollback = $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transaction['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rollback' => true, + ]); + + $this->assertEquals(200, $rollback['headers']['status-code']); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 7152925e3a..5a61ae7dc5 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -28,7 +28,7 @@ class RealtimeCustomClientTest extends Scope $userId = $user['$id'] ?? ''; $session = $user['session'] ?? ''; - $headers = [ + $headers = [ 'origin' => 'http://localhost', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session ]; @@ -3068,7 +3068,7 @@ class RealtimeCustomClientTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), [ 'documentId' => ID::unique(), - 'data' => [ 'name' => 'L2' ], + 'data' => ['name' => 'L2'], 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), @@ -3082,7 +3082,7 @@ class RealtimeCustomClientTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), [ 'documentId' => ID::unique(), - 'data' => [ 'name' => 'L1' ], + 'data' => ['name' => 'L1'], 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), @@ -3116,5 +3116,4 @@ class RealtimeCustomClientTest extends Scope $client->close(); } - } From 57e3e8f5f4104508dfa368083ec45efa54748194 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 4 Oct 2025 21:10:50 +1300 Subject: [PATCH 139/145] Fix test scope --- .../Legacy/Transactions/TransactionsBase.php | 30 +++---------------- .../TransactionsConsoleClientTest.php | 3 +- .../TransactionsCustomClientTest.php | 3 +- .../TransactionsCustomServerTest.php | 3 +- .../Transactions/TransactionsBase.php | 30 +++---------------- 5 files changed, 14 insertions(+), 55 deletions(-) diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php index 7c84295a4f..836f1ca966 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsBase.php @@ -1416,7 +1416,7 @@ trait TransactionsBase $transactionId = $transaction['body']['$id']; - // Try to update non-existent document + // Try to update non-existent document - should fail at staging time with early validation $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1433,20 +1433,9 @@ trait TransactionsBase ] ]); - $this->assertEquals(201, $response['headers']['status-code']); // Operation added + $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time - // Commit should fail - $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(404, $response['headers']['status-code']); // Document not found - - // Test delete non-existent document + // Test delete non-existent document - should also fail at staging time with early validation $transaction2 = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1470,18 +1459,7 @@ trait TransactionsBase ] ]); - $this->assertEquals(201, $response['headers']['status-code']); - - // Commit should fail - $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId2}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(404, $response['headers']['status-code']); // Document not found + $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time } /** diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php index 427e79fb3a..ef6e9d15d0 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsConsoleClientTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\Legacy\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; -class TransactionsConsoleClientTest +class TransactionsConsoleClientTest extends Scope { use TransactionsBase; use ProjectCustom; diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomClientTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomClientTest.php index 3ee685d778..cdc9a325dc 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomClientTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomClientTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\Legacy\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; -class TransactionsCustomClientTest +class TransactionsCustomClientTest extends Scope { use TransactionsBase; use ProjectCustom; diff --git a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomServerTest.php index 425f731ef3..7b3d092128 100644 --- a/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/Transactions/TransactionsCustomServerTest.php @@ -3,9 +3,10 @@ namespace Tests\E2E\Services\Databases\Legacy\Transactions; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -class TransactionsCustomServerTest +class TransactionsCustomServerTest extends Scope { use TransactionsBase; use ProjectCustom; diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index c972137cee..1ea8d0cc9a 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -1416,7 +1416,7 @@ trait TransactionsBase $transactionId = $transaction['body']['$id']; - // Try to update non-existent row + // Try to update non-existent row - should fail at staging time with early validation $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1433,20 +1433,9 @@ trait TransactionsBase ] ]); - $this->assertEquals(201, $response['headers']['status-code']); // Operation added + $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time - // Commit should fail - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(404, $response['headers']['status-code']); // Document not found - - // Test delete non-existent row + // Test delete non-existent row - should also fail at staging time with early validation $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1470,18 +1459,7 @@ trait TransactionsBase ] ]); - $this->assertEquals(201, $response['headers']['status-code']); - - // Commit should fail - $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'commit' => true - ]); - - $this->assertEquals(404, $response['headers']['status-code']); // Document not found + $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time } /** From 61b073a6a227be9ff4626718a183532440010a06 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 7 Oct 2025 18:53:38 +1300 Subject: [PATCH 140/145] Clean up --- composer.lock | 24 ++-- src/Appwrite/Databases/TransactionState.php | 29 ++--- .../Transactions/Operations/Create.php | 1 - .../Http/Databases/Transactions/Update.php | 106 +++++++----------- 4 files changed, 71 insertions(+), 89 deletions(-) diff --git a/composer.lock b/composer.lock index 8450c9df98..bad4ed32af 100644 --- a/composer.lock +++ b/composer.lock @@ -1927,16 +1927,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.46", + "version": "3.0.47", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", "shasum": "" }, "require": { @@ -2017,7 +2017,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" }, "funding": [ { @@ -2033,7 +2033,7 @@ "type": "tidelift" } ], - "time": "2025-06-26T16:29:55+00:00" + "time": "2025-10-06T01:07:24+00:00" }, { "name": "psr/container", @@ -3635,16 +3635,16 @@ }, { "name": "utopia-php/database", - "version": "2.2.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "19a3ab2ae99578861dd1a7b4fdbfb62b37d09447" + "reference": "a91e04080d7f13c35c4885dea0ffebc33cd33e1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/19a3ab2ae99578861dd1a7b4fdbfb62b37d09447", - "reference": "19a3ab2ae99578861dd1a7b4fdbfb62b37d09447", + "url": "https://api.github.com/repos/utopia-php/database/zipball/a91e04080d7f13c35c4885dea0ffebc33cd33e1f", + "reference": "a91e04080d7f13c35c4885dea0ffebc33cd33e1f", "shasum": "" }, "require": { @@ -3685,9 +3685,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/2.2.0" + "source": "https://github.com/utopia-php/database/tree/2.3.1" }, - "time": "2025-09-26T02:23:30+00:00" + "time": "2025-10-06T04:29:14+00:00" }, { "name": "utopia-php/detector", diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 2d35ce92ae..522e160a87 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -45,32 +45,27 @@ class TransactionState ?string $transactionId = null, array $queries = [] ): Document { - // If no transaction, use normal database retrieval if ($transactionId === null) { return $this->dbForProject->getDocument($collectionId, $documentId, $queries); } $state = $this->getTransactionState($transactionId); - // Check if document exists in transaction state if (isset($state[$collectionId][$documentId])) { $docState = $state[$collectionId][$documentId]; if (!$docState['exists']) { - // Document was deleted in transaction return new Document(); } if ($docState['action'] === 'create') { - // Document was created in transaction, return the created version return $this->applyProjection($docState['document'], $queries); } if ($docState['action'] === 'update' || $docState['action'] === 'upsert') { - // This is an update to an existing document, merge with committed version + // Merge with committed version $committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries); if (!$committedDoc->isEmpty()) { - // Apply the updates from transaction foreach ($docState['document']->getAttributes() as $key => $value) { if ($key !== '$id') { $committedDoc->setAttribute($key, $value); @@ -79,13 +74,11 @@ class TransactionState // Reapply projection in case transaction added new fields return $this->applyProjection($committedDoc, $queries); } elseif ($docState['action'] === 'upsert') { - // Upsert created a new document since committed doc doesn't exist return $this->applyProjection($docState['document'], $queries); } } } - // Document not affected by transaction, return committed version return $this->dbForProject->getDocument($collectionId, $documentId, $queries); } @@ -307,18 +300,21 @@ class TransactionState /** * Apply bulk upsert to documents in transaction state * - * This allows bulk operations within a transaction to see each other's changes. + * 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 + * @param array $documents Array of Document objects to upsert (can be partial) * @param array &$state Transaction state (passed by reference) - * @return void + * @return array Merged documents ready for database upsert */ public function applyBulkUpsertToState( string $collectionId, array $documents, array &$state - ): void { + ): array { + $mergedDocuments = []; + foreach ($documents as $doc) { if (!($doc instanceof Document)) { continue; @@ -329,7 +325,7 @@ class TransactionState continue; } - // If document exists in state, update it; otherwise it will be handled by DB upsert + // If document exists in state, update it and use the merged version if (isset($state[$collectionId][$docId])) { // Apply updates to existing state document foreach ($doc->getArrayCopy() as $key => $value) { @@ -337,8 +333,15 @@ class TransactionState $state[$collectionId][$docId]->setAttribute($key, $value); } } + // Use the full merged document from state + $mergedDocuments[] = $state[$collectionId][$docId]; + } else { + // Document not in state - use original partial data for DB upsert + $mergedDocuments[] = $doc; } } + + return $mergedDocuments; } /** diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 5f6fa4f1a6..c3ba45bdce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -113,7 +113,6 @@ class Create extends Action throw new Exception(Exception::COLLECTION_NOT_FOUND); } - // Check if collection has relationships for bulk operations if (\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) { $hasRelationships = \array_filter( $collection->getAttribute('attributes', []), diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index d522c699a8..e33a731d0e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -103,7 +103,6 @@ class Update extends Action */ public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks): void { - if (!$commit && !$rollback) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true'); } @@ -128,8 +127,6 @@ class Update extends Action if ($commit) { $operations = []; - - // Track metrics for usage stats $totalOperations = 0; $databaseOperations = []; @@ -139,15 +136,12 @@ class Update extends Action 'status' => 'committing', ]))); - // Fetch operations ordered by sequence by default to replay operations in exact order they were created $operations = Authorization::skip(fn () => $dbForProject->find('transactionLogs', [ Query::equal('transactionInternalId', [$transaction->getSequence()]), Query::orderAsc(), Query::limit(PHP_INT_MAX), ])); - - // Track transaction state for cross-operation visibility $state = []; foreach ($operations as $operation) { @@ -159,7 +153,6 @@ class Update extends Action $action = $operation['action']; $data = $operation['data']; - // For delete operations, fetch the document before deleting for realtime events if ($action === 'delete' && $documentId && empty($data)) { $doc = $dbForProject->getDocument($collectionId, $documentId); if (!$doc->isEmpty()) { @@ -168,7 +161,6 @@ class Update extends Action } } - // Track operations for stats $totalOperations++; $databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1; @@ -176,7 +168,6 @@ class Update extends Action $data = $data->getArrayCopy(); } - // Execute the operation based on its type switch ($action) { case 'create': $this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state); @@ -217,44 +208,37 @@ class Update extends Action new Document(['status' => 'committed']) )); - // Clear the transaction logs $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); }); } catch (NotFoundException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); } catch (DuplicateException|ConflictException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::TRANSACTION_CONFLICT, previous: $e); } catch (StructureException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (LimitException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, $e->getMessage()); } catch (TransactionException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::TRANSACTION_FAILED, $e->getMessage()); } catch (QueryException $e) { - // Transaction has been rolled back, now mark it as failed Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); @@ -264,7 +248,6 @@ class Update extends Action $queueForStatsUsage ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations); - // Add per-database metrics foreach ($databaseOperations as $sequence => $count) { $queueForStatsUsage->addMetric( str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES), @@ -272,8 +255,6 @@ class Update extends Action ); } - // Trigger realtime events for each operation - foreach ($operations as $operation) { $databaseInternalId = $operation['databaseInternalId']; $collectionInternalId = $operation['collectionInternalId']; @@ -316,7 +297,6 @@ class Update extends Action $eventAction = 'create'; $docId = $documentId ?? $data['$id'] ?? null; if ($docId) { - // Fetch the created document from the database $doc = $dbForProject->getDocument($collectionId, $docId); if (!$doc->isEmpty()) { $documentsToTrigger[] = $doc; @@ -328,7 +308,6 @@ class Update extends Action case 'decrement': $eventAction = 'update'; if ($documentId) { - // Fetch the updated document from the database $doc = $dbForProject->getDocument($collectionId, $documentId); if (!$doc->isEmpty()) { $documentsToTrigger[] = $doc; @@ -338,15 +317,13 @@ class Update extends Action case 'delete': $eventAction = 'delete'; if ($documentId && !empty($data)) { - // For delete, use the fetched document data (fetched before deletion) $documentsToTrigger[] = new Document(array_merge($data, ['$id' => $documentId])); } break; case 'upsert': - $eventAction = 'update'; // Upsert is treated as update for events + $eventAction = 'update'; $docId = $documentId ?? $data['$id'] ?? null; if ($docId) { - // Fetch the upserted document from the database $doc = $dbForProject->getDocument($collectionId, $docId); if (!$doc->isEmpty()) { $documentsToTrigger[] = $doc; @@ -360,15 +337,11 @@ class Update extends Action break; } - // Trigger events for each document - $eventString = "databases.[databaseId].{$contextKey}s.[{$groupId}].{$resourcePlural}.[{$resourceId}]." . $eventAction; $queueForEvents->setEvent($eventString); foreach ($documentsToTrigger as $doc) { - - // Add table/collection IDs to the payload for realtime channels $payload = $doc->getArrayCopy(); $payload['$tableId'] = $collection->getId(); $payload['$collectionId'] = $collection->getId(); @@ -464,7 +437,6 @@ class Update extends Action $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { - // Update the state document directly without timestamp wrapper $state[$collectionId][$documentId] = $dbForProject->updateDocument( $collectionId, $documentId, @@ -473,7 +445,6 @@ class Update extends Action return; } - // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { $document = $dbForProject->updateDocument( $collectionId, @@ -510,15 +481,21 @@ class Update extends Action $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { - // Upsert the state document directly without timestamp wrapper + // Merge partial upsert data with full document from transaction state + $existingDoc = $state[$collectionId][$documentId]; + foreach ($data as $key => $value) { + if ($key !== '$id') { + $existingDoc->setAttribute($key, $value); + } + } + $state[$collectionId][$documentId] = $dbForProject->upsertDocument( $collectionId, - new Document($data), + $existingDoc, ); return; } - // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { $state[$collectionId][$documentId] = $dbForProject->upsertDocument( $collectionId, @@ -549,13 +526,11 @@ class Update extends Action $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { - // Delete without timestamp wrapper $dbForProject->deleteDocument($collectionId, $documentId); unset($state[$collectionId][$documentId]); return; } - // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, &$state) { $deleted = $dbForProject->deleteDocument($collectionId, $documentId); if (!$deleted) { @@ -591,7 +566,6 @@ class Update extends Action $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { - // Increment without timestamp wrapper $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( collection: $collectionId, id: $documentId, @@ -602,7 +576,6 @@ class Update extends Action return; } - // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data) { $dbForProject->increaseDocumentAttribute( collection: $collectionId, @@ -638,7 +611,6 @@ class Update extends Action $dependent = isset($state[$collectionId][$documentId]); if ($dependent) { - // Decrement without timestamp wrapper $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( collection: $collectionId, id: $documentId, @@ -649,8 +621,6 @@ class Update extends Action return; } - // Use timestamp wrapper for independent operations - // Use timestamp wrapper for independent operations $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { $state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute( collection: $collectionId, @@ -681,7 +651,6 @@ class Update extends Action array &$state ): void { $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { - // Convert data arrays to Document objects if needed $documents = \array_map(function ($doc) { return $doc instanceof Document ? $doc : new Document($doc); }, $data); @@ -721,29 +690,50 @@ class Update extends Action $queries = Query::parseQueries($data['queries'] ?? []); $updateData = new Document($data['data']); - // First, update documents in the committed database + $dependentDocs = []; + + $transactionState->applyBulkUpdateToState($collectionId, $updateData, $queries, $state); + + // Clone the document before passing to updateDocuments to prevent mutation + // The database layer mutates the input document, which would corrupt transaction state $dbForProject->updateDocuments( $collectionId, - $updateData, + clone $updateData, $queries, - onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt) { - // Check if this document was created/modified in this transaction + onNext: function (Document $updated, Document $old) use (&$state, $collectionId, $createdAt, &$dependentDocs) { $dependent = isset($state[$collectionId][$updated->getId()]); - // If not in transaction state, check for timestamp conflicts - if (!$dependent) { + if ($dependent) { + $dependentDocs[] = $updated->getId(); + } else { $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); if ($oldUpdatedAt > $createdAt) { throw new ConflictException('Document was updated after the request timestamp'); } + $state[$collectionId][$updated->getId()] = $updated; } - - $state[$collectionId][$updated->getId()] = $updated; } ); - // Also update documents in the transaction state that match the query - $transactionState->applyBulkUpdateToState($collectionId, $updateData, $queries, $state); + // Re-write dependent documents from state to database to fix partial updates + if (!empty($dependentDocs)) { + $documentsToRewrite = []; + foreach ($dependentDocs as $docId) { + if (isset($state[$collectionId][$docId])) { + $documentsToRewrite[] = $state[$collectionId][$docId]; + } + } + + if (!empty($documentsToRewrite)) { + $dbForProject->upsertDocuments( + $collectionId, + $documentsToRewrite, + onNext: function (Document $upserted) use (&$state, $collectionId) { + $state[$collectionId][$upserted->getId()] = $upserted; + } + ); + } + } } /** @@ -767,25 +757,19 @@ class Update extends Action \DateTime $createdAt, array &$state ): void { - // Convert data arrays to Document objects if needed $documents = \array_map(function ($doc) { return $doc instanceof Document ? $doc : new Document($doc); }, $data); - // First, apply upserts to documents in the transaction state - // This ensures documents created in this transaction are updated properly - $transactionState->applyBulkUpsertToState($collectionId, $documents, $state); + $mergedDocuments = $transactionState->applyBulkUpsertToState($collectionId, $documents, $state); - // Then run bulk upsert on committed database, checking manually in callback $dbForProject->upsertDocuments( $collectionId, - $documents, + $mergedDocuments, onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) { if ($old !== null) { - // This is an update - check if document was created/modified in this transaction $dependent = isset($state[$collectionId][$upserted->getId()]); - // If not in transaction state, check for timestamp conflicts if (!$dependent) { $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); if ($oldUpdatedAt > $createdAt) { @@ -794,7 +778,6 @@ class Update extends Action } } - // If $old is null, this is a create operation - no timestamp check needed $state[$collectionId][$upserted->getId()] = $upserted; } ); @@ -830,7 +813,6 @@ class Update extends Action onNext: function (Document $deleted, Document $old) use (&$state, $collectionId, $createdAt) { $dependent = isset($state[$collectionId][$deleted->getId()]); - // If not in transaction state, check for timestamp conflicts if (!$dependent) { $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); if ($oldUpdatedAt > $createdAt) { @@ -838,14 +820,12 @@ class Update extends Action } } - // Remove from state after successful deletion if (isset($state[$collectionId][$deleted->getId()])) { unset($state[$collectionId][$deleted->getId()]); } } ); - // Also delete documents in the transaction state that match the query $transactionState->applyBulkDeleteToState($collectionId, $queries, $state); } } From eb7306a4fa7f4105d8afbe9dc39e6eae077e699c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 7 Oct 2025 22:17:59 +1300 Subject: [PATCH 141/145] Add missing state handlers --- src/Appwrite/Databases/TransactionState.php | 105 +++++++++++++++ .../Transactions/TransactionsBase.php | 123 ++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 8df2bc657c..23dc6fc2e9 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -423,6 +423,33 @@ class TransactionState ]; 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) { @@ -437,6 +464,84 @@ class TransactionState } } 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; } } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index 1ea8d0cc9a..e50bb9f2bf 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -4613,4 +4613,127 @@ trait TransactionsBase $this->assertEquals(404, $response['headers']['status-code']); } + + /** + * Test that bulkUpdate can match documents created in the same transaction + * This tests the fix for the bug where applyBulkUpdateToState was treating + * state entries as Documents instead of arrays with 'document' keys + */ + public function testBulkUpdateMatchesCreatedDocsInSameTransaction(): void + { + $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' => 'BulkUpdateStateDB' + ]); + + $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' => 'TestTable', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'flag', + 'size' => 256, + 'required' => false, + ]); + + sleep(3); + + $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']; + + // Create 3 documents with status='pending' in transaction + $docIds = []; + for ($i = 1; $i <= 3; $i++) { + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => 'test_' . $i, + 'data' => [ + 'status' => 'pending' + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $docIds[] = $response['body']['$id']; + } + + // Bulk update all documents with status='pending' to add flag='processed' + // This should match all 3 documents created above in the same transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'flag' => 'processed' + ], + 'queries' => [Query::equal('status', ['pending'])->toString()], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify all 3 documents have the flag set + foreach ($docIds as $docId) { + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('pending', $response['body']['status']); + $this->assertEquals('processed', $response['body']['flag'], 'Bulk update should have matched document created in same transaction'); + } + } } From 066e0bc236a8bc2a38345effe3d907621cc34ee4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 7 Oct 2025 23:33:19 +1300 Subject: [PATCH 142/145] Validate inc/dec operation value is numeric --- src/Appwrite/Utopia/Database/Validator/Operation.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php index 4e421689e3..e6884ac677 100644 --- a/src/Appwrite/Utopia/Database/Validator/Operation.php +++ b/src/Appwrite/Utopia/Database/Validator/Operation.php @@ -202,6 +202,11 @@ class Operation extends Validator $this->description = "Key '{$attributeKey}' is required in data for {$action}"; return false; } + // Validate 'value' is numeric if provided (defaults to 1 if omitted) + if (\array_key_exists('value', $value['data']) && !\is_numeric($value['data']['value'])) { + $this->description = "Key 'value' must be a numeric value for {$action}"; + return false; + } } return true; From dcad82c46b9e70ecf007188bda6c84522ca54fff Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 9 Oct 2025 13:45:19 +1300 Subject: [PATCH 143/145] Update db --- composer.lock | 13 +++++++------ .../Http/Databases/Transactions/Update.php | 5 +---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/composer.lock b/composer.lock index d4f1f85151..1afa3f5109 100644 --- a/composer.lock +++ b/composer.lock @@ -4566,16 +4566,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.13", + "version": "0.18.14", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "3d8ce53ae042173bf230445e996056c5f65ded22" + "reference": "4f14ec952c6f4006dd0613e55bbf7631814fbc00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/3d8ce53ae042173bf230445e996056c5f65ded22", - "reference": "3d8ce53ae042173bf230445e996056c5f65ded22", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/4f14ec952c6f4006dd0613e55bbf7631814fbc00", + "reference": "4f14ec952c6f4006dd0613e55bbf7631814fbc00", "shasum": "" }, "require": { @@ -4618,9 +4618,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.13" + "source": "https://github.com/utopia-php/storage/tree/0.18.14" }, - "time": "2025-05-26T13:10:35+00:00" + "time": "2025-10-07T10:21:47+00:00" }, { "name": "utopia-php/swoole", @@ -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" }, { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index e33a731d0e..ec0fab2486 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -263,7 +263,6 @@ class Update extends Action $documentId = $operation['documentId']; $data = $operation['data']; - if ($data instanceof Document) { $data = $data->getArrayCopy(); } @@ -351,9 +350,7 @@ class Update extends Action ->setParam('rowId', $doc->getId()) ->setPayload($payload); - $project = $queueForEvents->getProject(); - $result = $queueForRealtime->from($queueForEvents)->trigger(); - + $queueForRealtime->from($queueForEvents)->trigger(); $queueForFunctions->from($queueForEvents)->trigger(); $queueForWebhooks->from($queueForEvents)->trigger(); } From 3d3f50064ddb4f520bd627c4181cbedb0bb57a31 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 9 Oct 2025 15:50:18 +1300 Subject: [PATCH 144/145] Force set state on increment --- .../Http/Databases/Transactions/Update.php | 4 +- .../Transactions/TransactionsBase.php | 440 ++++++++++++++++++ 2 files changed, 442 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index ec0fab2486..311a5c6e7a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -573,8 +573,8 @@ class Update extends Action return; } - $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data) { - $dbForProject->increaseDocumentAttribute( + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { + $state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute( collection: $collectionId, id: $documentId, attribute: $data[$this->getAttributeKey()], diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index e50bb9f2bf..dc18338aa0 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -3732,6 +3732,446 @@ trait TransactionsBase $this->assertEquals(0, $row['body']['score']); } + /** + * Test increment followed by update (read-your-writes) + * This test ensures that after an increment operation, subsequent operations + * in the same transaction can see the incremented value in the transaction state. + */ + public function testIncrementThenUpdate(): 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' => 'IncrementUpdateTestDB' + ]); + + $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' => 'CounterTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add columns + $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' => 'counter', + 'required' => false, + 'default' => 0, + ]); + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + // Create initial row + $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_row', + 'data' => [ + 'counter' => 10, + 'status' => 'initial' + ] + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $transactionId = $transaction['body']['$id']; + + // Add operations: increment then update + // The update operation needs to see the document in transaction state + // to properly merge the changes + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'increment', + 'rowId' => 'test_row', + 'data' => [ + 'column' => 'counter', + 'value' => 5, + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'update', + 'rowId' => 'test_row', + 'data' => [ + 'status' => 'updated' + ] + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify final values - both increment and update should be applied + $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test_row", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals(15, $row['body']['counter'], 'Counter should be incremented: 10 + 5 = 15'); + $this->assertEquals('updated', $row['body']['status'], 'Status should be updated'); + } + + public function testBulkUpdateWithDependentDocuments(): 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' => 'BulkUpdateDependentDB' + ]); + + $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' => 'TestTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Add columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + // Create transaction + $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $transactionId = $transaction['body']['$id']; + + // Create a document, then bulk update it - this triggers the state structure bug + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'create', + 'rowId' => 'doc1', + 'data' => [ + 'status' => 'pending' + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkUpdate', + 'data' => [ + 'queries' => [], + 'data' => [ + 'status' => 'approved' + ] + ] + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code'], 'Bulk update should succeed on dependent documents'); + + // Verify the document was updated + $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals('approved', $row['body']['status']); + } + + /** + * Test bulk delete with dependent documents (Bug #2 regression test) + */ + public function testBulkDeleteWithDependentDocuments(): void + { + $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' => 'BulkDeleteDependentDB' + ]); + + $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' => 'TestTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + $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']; + + // Create then bulk delete + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'create', + 'rowId' => 'doc1', + 'data' => [ + 'name' => 'Test' + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkDelete', + 'data' => [ + 'queries' => [], + ] + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code'], 'Bulk delete should succeed on dependent documents'); + + // Verify document was deleted + $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(0, $rows['body']['total']); + } + + /** + * Test bulk upsert with dependent documents (Bug #3 regression test) + */ + public function testBulkUpsertWithDependentDocuments(): void + { + $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' => 'BulkUpsertDependentDB' + ]); + + $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' => 'TestTable', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'status', + 'size' => 50, + 'required' => false, + ]); + + sleep(2); + + $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']; + + // Create then bulk upsert same document + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'operations' => [ + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'create', + 'rowId' => 'doc1', + 'data' => [ + 'status' => 'pending' + ] + ], + [ + 'databaseId' => $databaseId, + 'tableId' => $tableId, + 'action' => 'bulkUpsert', + 'data' => [ + [ + '$id' => 'doc1', + 'status' => 'approved' + ] + ] + ], + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code'], 'Bulk upsert should succeed on dependent documents'); + + $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $row['headers']['status-code']); + $this->assertEquals('approved', $row['body']['status']); + } + /** * Test bulk update operations in transaction */ From 8193f0fcac1db1c8ca74a66fce53a7d80d39a8ed Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 9 Oct 2025 16:24:25 +1300 Subject: [PATCH 145/145] Ensure create/upsert stores in state by generated ID not unique string --- .../Http/Databases/Transactions/Update.php | 8 +- .../Transactions/TransactionsBase.php | 139 ++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index 311a5c6e7a..5d29bba34b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -403,10 +403,11 @@ class Update extends Action $data['$id'] = $documentId; } $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { - $state[$collectionId][$data['$id']] = $dbForProject->createDocument( + $doc = $dbForProject->createDocument( $collectionId, new Document($data), ); + $state[$collectionId][$doc->getId()] = $doc; }); } @@ -493,11 +494,12 @@ class Update extends Action return; } - $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) { - $state[$collectionId][$documentId] = $dbForProject->upsertDocument( + $dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) { + $doc = $dbForProject->upsertDocument( $collectionId, new Document($data), ); + $state[$collectionId][$doc->getId()] = $doc; }); } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index dc18338aa0..b64d249e97 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -5176,4 +5176,143 @@ trait TransactionsBase $this->assertEquals('processed', $response['body']['flag'], 'Bulk update should have matched document created in same transaction'); } } + + /** + * Test upsert with auto-generated ID followed by update + * This tests that the transaction state properly stores the document under its actual ID, + * not under null when the ID is auto-generated + */ + public function testUpsertAutoIdThenUpdate(): 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' => 'UpsertAutoIDTestDB' + ]); + + $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' => 'TestCollection', + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ]); + + $tableId = $table['body']['$id']; + + // Create columns + $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->client->call(Client::METHOD_POST, "/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' => 'counter', + 'required' => false, + 'min' => 0, + 'max' => 10000, + ]); + + sleep(3); + + // 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())); + + $transactionId = $transaction['body']['$id']; + + // First create a document in the transaction + $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Initial document', + 'counter' => 5 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $docId = $response['body']['$id']; + + // Now upsert the same document using ID::unique() in the path + // The database will recognize it exists and update it, generating a new auto ID if needed + // This tests that handleUpsertOperation properly captures the actual document ID + $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Upserted in transaction', + 'counter' => 10 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Now try to update the same document again in the same transaction + // This verifies that the upsert properly stored the document under its actual ID in state + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'name' => 'Updated after upsert', + 'counter' => 20 + ], + 'transactionId' => $transactionId + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Commit transaction + $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'commit' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + // Verify the document has the final updated values + $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Updated after upsert', $response['body']['name']); + $this->assertEquals(20, $response['body']['counter']); + } }