diff --git a/.env b/.env
index 0df9cb42f4..9abfa756e1 100644
--- a/.env
+++ b/.env
@@ -39,7 +39,7 @@ _APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
_APP_REDIS_USER=
-COMPOSE_PROFILES=mongodb
+COMPOSE_PROFILES=mariadb,mongodb,postgresql
_APP_DB_ADAPTER=mongodb
_APP_DB_HOST=mongodb
_APP_DB_PORT=27017
@@ -47,6 +47,15 @@ _APP_DB_SCHEMA=appwrite
_APP_DB_USER=user
_APP_DB_PASS=password
_APP_DB_ROOT_PASS=rootsecretpassword
+_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
+_APP_DB_HOST_DOCUMENTSDB=mongodb
+_APP_DB_PORT_DOCUMENTSDB=27017
+_APP_DB_ADAPTER_VECTORSDB=postgresql
+_APP_DB_HOST_VECTORSDB=postgresql
+_APP_DB_PORT_VECTORSDB=5432
+_APP_EMBEDDING_MODELS=embeddinggemma
+_APP_EMBEDDING_ENDPOINT='http://ollama:11434/api/embed'
+_APP_EMBEDDING_TIMEOUT=30000
_APP_STORAGE_DEVICE=Local
_APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a6ae039f0..aa6dbe2bc3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -394,7 +394,8 @@ jobs:
Webhooks,
VCS,
Messaging,
- Migrations
+ Migrations,
+ Project
]
include:
- service: Databases
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index cd9b3827e7..5cbec8f867 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -16,7 +16,7 @@ jobs:
- name: Build the Docker image
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
- name: Run Trivy vulnerability scanner on image
- uses: aquasecurity/trivy-action@0.20.0
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: 'appwrite_image:latest'
format: 'sarif'
@@ -35,7 +35,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
- name: Run Trivy vulnerability scanner on filesystem
- uses: aquasecurity/trivy-action@0.20.0
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: 'fs'
format: 'sarif'
diff --git a/app/config/collections.php b/app/config/collections.php
index a74e079dce..3af20ff2ac 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -4,6 +4,7 @@
$common = include __DIR__ . '/collections/common.php';
$projects = include __DIR__ . '/collections/projects.php';
$databases = include __DIR__ . '/collections/databases.php';
+$vectorsdb = include __DIR__ . '/collections/vectorsdb.php';
$platform = include __DIR__ . '/collections/platform.php';
$logs = include __DIR__ . '/collections/logs.php';
@@ -26,6 +27,7 @@ unset($common['files']);
$collections = [
'buckets' => $buckets,
'databases' => $databases,
+ 'vectorsdb' => $vectorsdb,
'projects' => array_merge_recursive($projects, $common),
'console' => array_merge_recursive($platform, $common),
'logs' => $logs,
diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php
index 55dceb9b40..b41e8f0fd5 100644
--- a/app/config/collections/projects.php
+++ b/app/config/collections/projects.php
@@ -61,6 +61,15 @@ return [
'array' => false,
'filters' => [],
],
+ [
+ '$id' => ID::custom('database'),
+ 'type' => Database::VAR_STRING,
+ 'size' => 128,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ]
],
'indexes' => [
[
diff --git a/app/config/collections/vectorsdb.php b/app/config/collections/vectorsdb.php
new file mode 100644
index 0000000000..817863cffa
--- /dev/null
+++ b/app/config/collections/vectorsdb.php
@@ -0,0 +1,165 @@
+ [
+ '$collection' => ID::custom('databases'),
+ '$id' => ID::custom('collections'),
+ 'name' => 'Collections',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('databaseInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('databaseId'),
+ 'type' => Database::VAR_STRING,
+ 'signed' => true,
+ 'size' => Database::LENGTH_KEY,
+ 'format' => '',
+ 'filters' => [],
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ ],
+ [
+ '$id' => ID::custom('name'),
+ 'type' => Database::VAR_STRING,
+ 'size' => 256,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('dimension'),
+ 'type' => Database::VAR_INTEGER,
+ 'size' => 0,
+ 'required' => true,
+ 'signed' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('enabled'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'signed' => true,
+ 'size' => 0,
+ 'format' => '',
+ 'filters' => [],
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ ],
+ [
+ '$id' => ID::custom('documentSecurity'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'signed' => true,
+ 'size' => 0,
+ 'format' => '',
+ 'filters' => [],
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ ],
+ [
+ '$id' => ID::custom('attributes'),
+ 'type' => Database::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['subQueryAttributes'],
+ ],
+ [
+ '$id' => ID::custom('indexes'),
+ 'type' => Database::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['subQueryIndexes'],
+ ],
+ [
+ '$id' => ID::custom('search'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'defaultAttributes' => [
+ [
+ '$id' => ID::custom('embeddings'),
+ 'type' => Database::VAR_VECTOR,
+ 'required' => true,
+ 'signed' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('metadata'),
+ 'type' => Database::VAR_OBJECT,
+ 'default' => [],
+ 'required' => false,
+ 'size' => 0,
+ 'signed' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_fulltext_search'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['search'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_name'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['name'],
+ 'lengths' => [256],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_enabled'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['enabled'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_documentSecurity'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['documentSecurity'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ ],
+ 'defaultIndexes' => [
+ // not creating default indexes on the embeddings as it depends on the type of query users using the most
+ [
+ '$id' => ID::custom('_key_metadata'),
+ 'type' => Database::INDEX_OBJECT,
+ 'attributes' => ['metadata'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ ]
+ ]
+];
diff --git a/app/config/errors.php b/app/config/errors.php
index 278dbb3458..ec2593d207 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -1206,6 +1206,11 @@ return [
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
'code' => 409,
],
+ Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED => [
+ 'name' => Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED,
+ 'description' => 'The specified database type is not supported for CSV import or export operations.',
+ 'code' => 400,
+ ],
/** Realtime */
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
diff --git a/app/config/roles.php b/app/config/roles.php
index 4473176c23..116e8ac932 100644
--- a/app/config/roles.php
+++ b/app/config/roles.php
@@ -62,6 +62,8 @@ $admins = [
'devKeys.write',
'webhooks.read',
'webhooks.write',
+ 'project.read',
+ 'project.write',
'locale.read',
'avatars.read',
'health.read',
diff --git a/app/config/scopes/organization.php b/app/config/scopes/organization.php
index ca4160881d..8d85662652 100644
--- a/app/config/scopes/organization.php
+++ b/app/config/scopes/organization.php
@@ -31,12 +31,4 @@ return [
"description" =>
"Access to create, update, and delete project\'s development keys",
],
- "webhooks.read" => [
- "description" =>
- "Access to read project\'s webhooks",
- ],
- "webhooks.write" => [
- "description" =>
- "Access to create, update, and delete project\'s webhooks",
- ],
];
diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php
index 1f318b0376..f5d8461aff 100644
--- a/app/config/scopes/project.php
+++ b/app/config/scopes/project.php
@@ -180,4 +180,12 @@ return [ // List of publicly visible scopes
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
+ "project.read" => [
+ "description" =>
+ "Access to read project\'s information",
+ ],
+ "project.write" => [
+ "description" =>
+ "Access to update project\'s information",
+ ],
];
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 6d33b45f0b..3d7db8f457 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -209,6 +209,22 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
+ // Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
+ $oauthProvider = null;
+ try {
+ $jwtDecoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
+ $payload = $jwtDecoder->decode($secret);
+
+ if (empty($payload['provider'])) {
+ throw new Exception(Exception::USER_INVALID_TOKEN);
+ }
+
+ $oauthProvider = $payload['provider'];
+ $secret = $payload['secret'];
+ } catch (\Ahc\Jwt\JWTException) {
+ // Not a JWT — use secret as-is (non-OAuth flows)
+ }
+
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
@@ -220,6 +236,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
?: $userFromRequest->tokenVerify(null, $secret, $proofForCode);
if (!$verifiedToken) {
+ // Could mean invalid/expired JWT, or expired secret
+ throw new Exception(Exception::USER_INVALID_TOKEN);
+ }
+
+ // OAuth2 tokens must have a provider from the JWT
+ if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_OAUTH2 && $oauthProvider === null) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
@@ -245,7 +267,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
- TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2,
+ TOKEN_TYPE_OAUTH2 => $oauthProvider,
default => SESSION_PROVIDER_TOKEN,
};
$session = new Document(array_merge(
@@ -1899,7 +1921,12 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setParam('tokenId', $token->getId())
;
- $query['secret'] = $secret;
+ // Wrap secret in a JWT that also carries the provider name
+ $jwtEncoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
+ $query['secret'] = $jwtEncoder->encode([
+ 'secret' => $secret,
+ 'provider' => $provider,
+ ]);
$query['userId'] = $user->getId();
// If the `token` param is not set, we persist the session in a cookie
diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php
index bfb73189b5..5a87293b49 100644
--- a/app/controllers/api/migrations.php
+++ b/app/controllers/api/migrations.php
@@ -43,6 +43,16 @@ use Utopia\Validator\WhiteList;
include_once __DIR__ . '/../shared/api.php';
+function getDatabaseTransferResourceServices(string $databaseType)
+{
+ return match($databaseType) {
+ DATABASE_TYPE_LEGACY,
+ DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
+ DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
+ DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
+ };
+}
+
Http::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Create Appwrite migration')
@@ -427,8 +437,16 @@ Http::post('/v1/migrations/csv/imports')
throw new \Exception('Unable to copy file');
}
+ // getting databasetype
+ $resources = explode(':', $resourceId);
+ $databaseId = $resources[0];
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+ $databaseType = $database->getAttribute('type');
+ if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
+ throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
+ }
$fileSize = $deviceForMigrations->getFileSize($newPath);
- $resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
+ $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => $migrationId,
@@ -557,13 +575,23 @@ Http::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
+ // getting databasetype
+ $resources = explode(':', $resourceId);
+ $databaseId = $resources[0];
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+ $databaseType = $database->getAttribute('type');
+ if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
+ throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
+ }
+ $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
+
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => CSV::getName(),
- 'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
+ 'resources' => $resources,
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
@@ -713,12 +741,11 @@ Http::get('/v1/migrations/appwrite/report')
->param('projectID', '', new Text(512), "Source's Project ID")
->param('key', '', new Text(512), "Source's API Key")
->inject('response')
- ->inject('dbForProject')
- ->inject('project')
- ->inject('user')
- ->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
+ ->inject('getDatabasesDB')
+ ->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response, callable $getDatabasesDB) {
+
try {
- $appwrite = new Appwrite($projectID, $endpoint, $key);
+ $appwrite = new Appwrite($projectID, $endpoint, $key, $getDatabasesDB);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
throw new Exception(
diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php
index d24519e3fb..054a7c8f0d 100644
--- a/app/controllers/api/project.php
+++ b/app/controllers/api/project.php
@@ -1,25 +1,15 @@
[
METRIC_NETWORK_REQUESTS,
@@ -80,11 +87,26 @@ Http::get('/v1/project/usage')
METRIC_USERS,
METRIC_EXECUTIONS,
METRIC_DATABASES_STORAGE,
+ METRIC_DATABASES_STORAGE_DOCUMENTSDB,
METRIC_EXECUTIONS_MB_SECONDS,
METRIC_BUILDS_MB_SECONDS,
METRIC_DATABASES_OPERATIONS_READS,
+ METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
METRIC_DATABASES_OPERATIONS_WRITES,
+ METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
METRIC_FILES_IMAGES_TRANSFORMED,
+ // VectorsDB time series
+ METRIC_DATABASES_VECTORSDB,
+ METRIC_COLLECTIONS_VECTORSDB,
+ METRIC_DOCUMENTS_VECTORSDB,
+ METRIC_DATABASES_STORAGE_VECTORSDB,
+ METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
+ METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
+ // Embeddings time series
+ METRIC_EMBEDDINGS_TEXT,
+ METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
+ METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
+ METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
]
];
@@ -357,8 +379,11 @@ Http::get('/v1/project/usage')
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
'documentsTotal' => $total[METRIC_DOCUMENTS],
'rowsTotal' => $total[METRIC_DOCUMENTS],
+ 'documentsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_DOCUMENTSDB],
'databasesTotal' => $total[METRIC_DATABASES],
+ 'documentsdbTotal' => $total[METRIC_DATABASES_DOCUMENTSDB],
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
+ 'documentsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
'usersTotal' => $total[METRIC_USERS],
'bucketsTotal' => $total[METRIC_BUCKETS],
'filesStorageTotal' => $total[METRIC_FILES_STORAGE],
@@ -367,10 +392,27 @@ Http::get('/v1/project/usage')
'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE],
'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS],
'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES],
+ 'documentsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
+ 'documentsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
+ 'vectorsdbDatabasesTotal' => $total[METRIC_DATABASES_VECTORSDB] ?? 0,
+ 'vectorsdbCollectionsTotal' => $total[METRIC_COLLECTIONS_VECTORSDB] ?? 0,
+ 'vectorsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_VECTORSDB] ?? 0,
+ 'vectorsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_VECTORSDB] ?? 0,
+ 'vectorsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? 0,
+ 'vectorsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? 0,
'executionsBreakdown' => $executionsBreakdown,
'bucketsBreakdown' => $bucketsBreakdown,
'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS],
'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES],
+ 'documentsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
+ 'documentsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
+ 'documentsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
+ 'vectorsdbDatabases' => $usage[METRIC_DATABASES_VECTORSDB] ?? [],
+ 'vectorsdbCollections' => $usage[METRIC_COLLECTIONS_VECTORSDB] ?? [],
+ 'vectorsdbDocuments' => $usage[METRIC_DOCUMENTS_VECTORSDB] ?? [],
+ 'vectorsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_VECTORSDB] ?? [],
+ 'vectorsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? [],
+ 'vectorsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? [],
'databasesStorageBreakdown' => $databasesStorageBreakdown,
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
@@ -380,230 +422,13 @@ Http::get('/v1/project/usage')
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED],
'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED],
+ 'embeddingsText' => $usage[METRIC_EMBEDDINGS_TEXT] ?? [],
+ 'embeddingsTextTokens' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? [],
+ 'embeddingsTextDuration' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? [],
+ 'embeddingsTextErrors' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? [],
+ 'embeddingsTextTotal' => $total[METRIC_EMBEDDINGS_TEXT] ?? 0,
+ 'embeddingsTextTokensTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? 0,
+ 'embeddingsTextDurationTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? 0,
+ 'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0,
]), Response::MODEL_USAGE_PROJECT);
});
-
-
-// Variables
-Http::post('/v1/project/variables')
- ->desc('Create variable')
- ->groups(['api'])
- ->label('scope', 'projects.write')
- ->label('audits.event', 'variable.create')
- ->label('sdk', new Method(
- namespace: 'project',
- group: null,
- name: 'createVariable',
- description: '/docs/references/project/create-variable.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_CREATED,
- model: Response::MODEL_VARIABLE,
- )
- ]
- ))
- ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
- ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
- ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
- ->inject('project')
- ->inject('response')
- ->inject('dbForProject')
- ->inject('dbForPlatform')
- ->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
- $variableId = ID::unique();
-
- $variable = new Document([
- '$id' => $variableId,
- '$permissions' => [
- Permission::read(Role::any()),
- Permission::update(Role::any()),
- Permission::delete(Role::any()),
- ],
- 'resourceInternalId' => '',
- 'resourceId' => '',
- 'resourceType' => 'project',
- 'key' => $key,
- 'value' => $value,
- 'secret' => $secret,
- 'search' => implode(' ', [$variableId, $key, 'project']),
- ]);
-
- try {
- $variable = $dbForProject->createDocument('variables', $variable);
- } catch (DuplicateException $th) {
- throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
- }
-
- $functions = $dbForProject->find('functions', [
- Query::limit(APP_LIMIT_SUBQUERY)
- ]);
-
- foreach ($functions as $function) {
- $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
- }
-
- $response
- ->setStatusCode(Response::STATUS_CODE_CREATED)
- ->dynamic($variable, Response::MODEL_VARIABLE);
- });
-
-Http::get('/v1/project/variables')
- ->desc('List variables')
- ->groups(['api'])
- ->label('scope', 'projects.read')
- ->label('sdk', new Method(
- namespace: 'project',
- group: null,
- name: 'listVariables',
- description: '/docs/references/project/list-variables.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_OK,
- model: Response::MODEL_VARIABLE_LIST,
- )
- ]
- ))
- ->inject('response')
- ->inject('dbForProject')
- ->action(function (Response $response, Database $dbForProject) {
- $variables = $dbForProject->find('variables', [
- Query::equal('resourceType', ['project']),
- Query::limit(APP_LIMIT_SUBQUERY)
- ]);
-
- $response->dynamic(new Document([
- 'variables' => $variables,
- 'total' => \count($variables),
- ]), Response::MODEL_VARIABLE_LIST);
- });
-
-Http::get('/v1/project/variables/:variableId')
- ->desc('Get variable')
- ->groups(['api'])
- ->label('scope', 'projects.read')
- ->label('sdk', new Method(
- namespace: 'project',
- group: null,
- name: 'getVariable',
- description: '/docs/references/project/get-variable.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_OK,
- model: Response::MODEL_VARIABLE,
- )
- ]
- ))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
- ->inject('response')
- ->inject('project')
- ->inject('dbForProject')
- ->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
- $variable = $dbForProject->getDocument('variables', $variableId);
- if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
- throw new Exception(Exception::VARIABLE_NOT_FOUND);
- }
-
- $response->dynamic($variable, Response::MODEL_VARIABLE);
- });
-
-Http::put('/v1/project/variables/:variableId')
- ->desc('Update variable')
- ->groups(['api'])
- ->label('scope', 'projects.write')
- ->label('sdk', new Method(
- namespace: 'project',
- group: null,
- name: 'updateVariable',
- description: '/docs/references/project/update-variable.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_OK,
- model: Response::MODEL_VARIABLE,
- )
- ]
- ))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
- ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
- ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
- ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
- ->inject('project')
- ->inject('response')
- ->inject('dbForProject')
- ->inject('dbForPlatform')
- ->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
- $variable = $dbForProject->getDocument('variables', $variableId);
- if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
- throw new Exception(Exception::VARIABLE_NOT_FOUND);
- }
-
- if ($variable->getAttribute('secret') === true && $secret === false) {
- throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
- }
-
- $variable
- ->setAttribute('key', $key)
- ->setAttribute('value', $value ?? $variable->getAttribute('value'))
- ->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
- ->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
-
- try {
- $dbForProject->updateDocument('variables', $variable->getId(), $variable);
- } catch (DuplicateException $th) {
- throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
- }
-
- $functions = $dbForProject->find('functions', [
- Query::limit(APP_LIMIT_SUBQUERY)
- ]);
-
- foreach ($functions as $function) {
- $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
- }
-
- $response->dynamic($variable, Response::MODEL_VARIABLE);
- });
-
-Http::delete('/v1/project/variables/:variableId')
- ->desc('Delete variable')
- ->groups(['api'])
- ->label('scope', 'projects.write')
- ->label('sdk', new Method(
- namespace: 'project',
- group: null,
- name: 'deleteVariable',
- description: '/docs/references/project/delete-variable.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_NOCONTENT,
- model: Response::MODEL_NONE,
- )
- ],
- contentType: ContentType::NONE
- ))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
- ->inject('project')
- ->inject('response')
- ->inject('dbForProject')
- ->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
- $variable = $dbForProject->getDocument('variables', $variableId);
- if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
- throw new Exception(Exception::VARIABLE_NOT_FOUND);
- }
-
- $dbForProject->deleteDocument('variables', $variable->getId());
-
- $functions = $dbForProject->find('functions', [
- Query::limit(APP_LIMIT_SUBQUERY)
- ]);
-
- foreach ($functions as $function) {
- $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
- }
-
- $response->noContent();
- });
diff --git a/app/controllers/general.php b/app/controllers/general.php
index 1a099c4bde..51cce37fee 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -1190,6 +1190,15 @@ Http::error()
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
+ $trace = $error->getTrace();
+
+ foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
+ $file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
+ $line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
+ $function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
+ Console::error("[$index] $file : $line -> $function()");
+ }
+
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index 90ac1bc378..f98b9ed454 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -486,6 +486,12 @@ Http::init()
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$route = $utopia->getRoute();
+ $path = $route->getMatchedPath();
+ $databaseType = match (true) {
+ str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+ str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
+ default => '',
+ };
if (
array_key_exists('rest', $project->getAttribute('apis', []))
diff --git a/app/http.php b/app/http.php
index 1302940856..517a1aa544 100644
--- a/app/http.php
+++ b/app/http.php
@@ -196,6 +196,8 @@ include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
+ $max = 15;
+ $sleep = 2;
$max = 15;
$sleep = 2;
$attempts = 0;
@@ -409,13 +411,29 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
});
$projectCollections = $collections['projects'];
+
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
+ $documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
+ $documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
+ $documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
+
+ $vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
+ $vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
+ $vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
+
$cache = $app->getResource('cache');
- foreach ($sharedTablesV2 as $hostname) {
+ // All shared tables V2 pools that need project metadata collections
+ $sharedTablesV2All = \array_values(\array_unique(\array_filter([
+ ...$sharedTablesV2,
+ ...$documentsSharedTablesV2,
+ ...$vectorSharedTablesV2,
+ ])));
+
+ foreach ($sharedTablesV2All as $hostname) {
Span::init('database.setup');
Span::add('database.hostname', $hostname);
diff --git a/app/init/constants.php b/app/init/constants.php
index c578bdbf9a..8fdd6d1a51 100644
--- a/app/init/constants.php
+++ b/app/init/constants.php
@@ -288,6 +288,45 @@ const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
+
+// documentsdb
+const METRIC_DATABASES_DOCUMENTSDB = 'documentsdb.databases';
+const METRIC_COLLECTIONS_DOCUMENTSDB = 'documentsdb.collections';
+const METRIC_DATABASES_STORAGE_DOCUMENTSDB = 'documentsdb.databases.storage';
+const METRIC_DATABASE_ID_COLLECTIONS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.collections';
+const METRIC_DATABASE_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.storage';
+const METRIC_DOCUMENTS_DOCUMENTSDB = 'documentsdb.documents';
+const METRIC_DATABASE_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.documents';
+const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.documents';
+const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
+const METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.databases.operations.reads';
+const METRIC_DATABASE_ID_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.reads';
+const METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.databases.operations.writes';
+const METRIC_DATABASE_ID_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.writes';
+
+// vectorsdb
+const METRIC_DATABASES_VECTORSDB = 'vectorsdb.databases';
+const METRIC_COLLECTIONS_VECTORSDB = 'vectorsdb.collections';
+const METRIC_DATABASES_STORAGE_VECTORSDB = 'vectorsdb.databases.storage';
+const METRIC_DATABASE_ID_COLLECTIONS_VECTORSDB = 'vectorsdb.{databaseInternalId}.collections';
+const METRIC_DATABASE_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.storage';
+const METRIC_DOCUMENTS_VECTORSDB = 'vectorsdb.documents';
+const METRIC_DATABASE_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.documents';
+const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.documents';
+const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
+const METRIC_DATABASES_OPERATIONS_READS_VECTORSDB = 'vectorsdb.databases.operations.reads';
+const METRIC_DATABASE_ID_OPERATIONS_READS_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.reads';
+const METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.databases.operations.writes';
+const METRIC_DATABASE_ID_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.writes';
+const METRIC_EMBEDDINGS_TEXT = 'embeddings.text';
+const METRIC_EMBEDDINGS_MODEL_TEXT = 'embeddings.text.{embeddingModel}';
+const METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR = 'embeddings.text.totalErrors';
+const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_ERROR = 'embeddings.text.{embeddingModel}.totalErrors';
+const METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION = 'embeddings.text.totalDuration';
+const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_DURATION = 'embeddings.text.{embeddingModel}.totalDuration';
+const METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS = 'embeddings.text.totalTokens';
+const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_TOKENS = 'embeddings.text.{embeddingModel}.totalTokens';
+
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
@@ -380,6 +419,7 @@ const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_VCS = 'vcs';
+const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
@@ -401,3 +441,16 @@ const CACHE_RECONNECT_RETRY_DELAY = 1000;
// Project status
const PROJECT_STATUS_ACTIVE = 'active';
+
+// Database types
+const DATABASE_TYPE_LEGACY = 'legacy';
+const DATABASE_TYPE_TABLESDB = 'tablesdb';
+const DATABASE_TYPE_DOCUMENTSDB = 'documentsdb';
+const DATABASE_TYPE_VECTORSDB = 'vectorsdb';
+
+// CSV import/export allowed database types
+const CSV_ALLOWED_DATABASE_TYPES = [
+ DATABASE_TYPE_LEGACY,
+ DATABASE_TYPE_TABLESDB,
+ DATABASE_TYPE_VECTORSDB
+];
diff --git a/app/init/models.php b/app/init/models.php
index 6c90f08199..bf6d67dd95 100644
--- a/app/init/models.php
+++ b/app/init/models.php
@@ -22,6 +22,7 @@ use Appwrite\Utopia\Response\Model\AttributeLine;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeLongtext;
use Appwrite\Utopia\Response\Model\AttributeMediumtext;
+use Appwrite\Utopia\Response\Model\AttributeObject;
use Appwrite\Utopia\Response\Model\AttributePoint;
use Appwrite\Utopia\Response\Model\AttributePolygon;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeText;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeVarchar;
+use Appwrite\Utopia\Response\Model\AttributeVector;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
@@ -65,6 +67,7 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime;
use Appwrite\Utopia\Response\Model\DetectionVariable;
use Appwrite\Utopia\Response\Model\DevKey;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
+use Appwrite\Utopia\Response\Model\Embedding;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
@@ -136,6 +139,8 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
+use Appwrite\Utopia\Response\Model\UsageDocumentsDB;
+use Appwrite\Utopia\Response\Model\UsageDocumentsDBs;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
@@ -144,9 +149,12 @@ use Appwrite\Utopia\Response\Model\UsageSites;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageTable;
use Appwrite\Utopia\Response\Model\UsageUsers;
+use Appwrite\Utopia\Response\Model\UsageVectorsDB;
+use Appwrite\Utopia\Response\Model\UsageVectorsDBs;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
+use Appwrite\Utopia\Response\Model\VectorsDBCollection;
use Appwrite\Utopia\Response\Model\Webhook;
// General
@@ -211,9 +219,12 @@ Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIS
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
+Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
+Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
// Entities
Response::setModel(new Database());
+Response::setModel(new Embedding());
// Collection API Models
Response::setModel(new Collection());
@@ -237,6 +248,17 @@ Response::setModel(new AttributeText());
Response::setModel(new AttributeMediumtext());
Response::setModel(new AttributeLongtext());
+// DocumentsDB API Models
+Response::setModel(new UsageDocumentsDBs());
+Response::setModel(new UsageDocumentsDB());
+
+// VectorsDB API Models
+Response::setModel(new VectorsDBCollection());
+Response::setModel(new AttributeObject());
+Response::setModel(new AttributeVector());
+Response::setModel(new UsageVectorsDBs());
+Response::setModel(new UsageVectorsDB());
+
// Table API Models
Response::setModel(new Table());
Response::setModel(new Column());
diff --git a/app/init/registers.php b/app/init/registers.php
index 7b68c2af9a..7c2f822fdd 100644
--- a/app/init/registers.php
+++ b/app/init/registers.php
@@ -160,7 +160,6 @@ $register->set('pools', function () {
'pass' => System::getEnv('_APP_DB_PASS', ''),
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
]);
-
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
'scheme' => 'redis',
'host' => System::getEnv('_APP_REDIS_HOST', 'redis'),
@@ -169,6 +168,23 @@ $register->set('pools', function () {
'pass' => System::getEnv('_APP_REDIS_PASS', ''),
]);
+ $fallbackForDocumentsDB = 'db_main=' . AppwriteURL::unparse([
+ 'scheme' => System::getEnv('_APP_DB_ADAPTER_DOCUMENTSDB', 'mongodb'),
+ 'host' => System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'),
+ 'port' => System::getEnv('_APP_DB_PORT_DOCUMENTSDB', '27017'),
+ 'user' => System::getEnv('_APP_DB_USER', ''),
+ 'pass' => System::getEnv('_APP_DB_PASS', ''),
+ 'path' => System::getEnv('_APP_DB_SCHEMA', ''),
+ ]);
+ $fallbackForVectorsDB = 'db_main=' . AppwriteURL::unparse([
+ 'scheme' => System::getEnv('_APP_DB_ADAPTER_VECTORSDB', 'postgresql'),
+ 'host' => System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'),
+ 'port' => System::getEnv('_APP_DB_PORT_VECTORSDB', '5432'),
+ 'user' => System::getEnv('_APP_DB_USER', ''),
+ 'pass' => System::getEnv('_APP_DB_PASS', ''),
+ 'path' => System::getEnv('_APP_DB_SCHEMA', ''),
+ ]);
+
$connections = [
'console' => [
'type' => 'database',
@@ -180,13 +196,25 @@ $register->set('pools', function () {
'type' => 'database',
'dsns' => $fallbackForDB,
'multiple' => true,
- 'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
+ 'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
+ ],
+ 'documentsdb' => [
+ 'type' => 'database',
+ 'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_DOCUMENTSDB', $fallbackForDocumentsDB),
+ 'multiple' => true,
+ 'schemes' => ['mongodb'],
+ ],
+ 'vectorsdb' => [
+ 'type' => 'database',
+ 'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORSDB', $fallbackForVectorsDB),
+ 'multiple' => true,
+ 'schemes' => ['postgresql'],
],
'logs' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_LOGS', $fallbackForDB),
'multiple' => false,
- 'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
+ 'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
],
'publisher' => [
'type' => 'publisher',
diff --git a/app/init/resources.php b/app/init/resources.php
index 3465f22560..9883d6d644 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -32,6 +32,8 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
+use Utopia\Agents\Adapters\Ollama;
+use Utopia\Agents\Agent;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Argon2;
@@ -594,7 +596,7 @@ Http::setResource('authorization', function () {
return new Authorization();
}, []);
-Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) {
+Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -700,7 +702,31 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$dbForProject->getCache()->purge($cacheKey);
};
- $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
+ /**
+ * Prefix metrics with database type when applicable.
+ * Avoids prefixing for legacy and tablesdb types to preserve historical metrics.
+ */
+ $getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string {
+ if (
+ $databaseType === '' ||
+ $databaseType === DATABASE_TYPE_LEGACY ||
+ $databaseType === DATABASE_TYPE_TABLESDB
+ ) {
+ return $metric;
+ }
+
+ return $databaseType . '.' . $metric;
+ };
+
+ // Determine database type from request path, similar to api.php
+ $path = $request->getURI();
+ $databaseType = match (true) {
+ str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+ str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
+ default => '',
+ };
+
+ $usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) {
$value = 1;
switch ($event) {
@@ -732,7 +758,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$usage->addMetric(METRIC_SESSIONS, $value); // per project
break;
case $document->getCollection() === 'databases': // databases
- $usage->addMetric(METRIC_DATABASES, $value); // per project
+ $metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES);
+ $usage->addMetric($metric, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
@@ -741,9 +768,11 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
+ $collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS);
+ $databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS);
$usage
- ->addMetric(METRIC_COLLECTIONS, $value) // per project
- ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
+ ->addMetric($collectionMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
@@ -753,10 +782,13 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
+ $documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS);
+ $databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS);
+ $databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS);
$usage
- ->addMetric(METRIC_DOCUMENTS, $value) // per project
- ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
- ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
break;
case $document->getCollection() === 'buckets': // buckets
$usage->addMetric(METRIC_BUCKETS, $value); // per project
@@ -831,7 +863,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
-}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
+}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
@@ -852,6 +884,138 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
return $database;
}, ['pools', 'cache', 'authorization']);
+Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
+
+ return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
+ $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
+ $databaseType = $database->getAttribute('type', '');
+
+ try {
+ $databaseDSN = new DSN($databaseDSN);
+ } catch (\InvalidArgumentException) {
+ // for old databases migrated through patch script
+ // databaseDSN determines the adapter
+ $databaseDSN = new DSN('mysql://'.$databaseDSN);
+ }
+ try {
+ $dsn = new DSN($project->getAttribute('database'));
+ } catch (\InvalidArgumentException) {
+ // TODO: Temporary until all projects are using shared tables
+ $dsn = new DSN('mysql://' . $project->getAttribute('database'));
+ }
+
+ $pool = $pools->get($databaseDSN->getHost());
+
+ $adapter = new DatabasePool($pool);
+ $database = new Database($adapter, $cache);
+ $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
+
+ $database
+ ->setDatabase(APP_DATABASE)
+ ->setAuthorization($authorization)
+ ->setMetadata('host', \gethostname())
+ ->setMetadata('project', $project->getId())
+ ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
+ ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
+ // inside pools authorization needs to be set first
+ $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
+ if (\in_array($dsn->getHost(), $sharedTables)) {
+ $database
+ ->setSharedTables(true)
+ ->setTenant((int)$project->getSequence())
+ ->setNamespace($dsn->getParam('namespace'));
+ } else {
+ $database
+ ->setSharedTables(false)
+ ->setTenant(null)
+ ->setNamespace('_' . $project->getSequence());
+ }
+ $timeout = \intval($request->getHeader('x-appwrite-timeout'));
+ if (!empty($timeout) && Http::isDevelopment()) {
+ $database->setTimeout($timeout);
+ }
+
+ // Register database event listeners for usage stats collection
+ $documentsMetric = METRIC_DOCUMENTS;
+ $databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS;
+ $databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
+ if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
+ $documentsMetric = $databaseType. '.' .$documentsMetric;
+ $databaseIdDocumentsMetric = $databaseType. '.' .$databaseIdDocumentsMetric;
+ $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' .$databaseIdCollectionIdDocumentsMetric;
+ }
+ $database
+ ->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
+ $value = 1;
+
+ if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
+ $parts = explode('_', $document->getCollection());
+ $databaseInternalId = $parts[1] ?? 0;
+ $collectionInternalId = $parts[3] ?? 0;
+ $usage
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
+ }
+ })
+ ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
+ $value = -1;
+
+ if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
+ $parts = explode('_', $document->getCollection());
+ $databaseInternalId = $parts[1] ?? 0;
+ $collectionInternalId = $parts[3] ?? 0;
+ $usage
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
+ }
+ })
+ ->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
+ $value = $document->getAttribute('modified', 0);
+
+ if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
+ $parts = explode('_', $document->getCollection());
+ $databaseInternalId = $parts[1] ?? 0;
+ $collectionInternalId = $parts[3] ?? 0;
+ $usage
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
+ }
+ })
+ ->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
+ $value = -1 * $document->getAttribute('modified', 0);
+
+ if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
+ $parts = explode('_', $document->getCollection());
+ $databaseInternalId = $parts[1] ?? 0;
+ $collectionInternalId = $parts[3] ?? 0;
+ $usage
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
+ }
+ })
+ ->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
+ $value = $document->getAttribute('created', 0);
+
+ if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
+ $parts = explode('_', $document->getCollection());
+ $databaseInternalId = $parts[1] ?? 0;
+ $collectionInternalId = $parts[3] ?? 0;
+ $usage
+ ->addMetric($documentsMetric, $value) // per project
+ ->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
+ ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
+ }
+ });
+
+ return $database;
+ };
+
+}, ['pools','cache','project','request','usage','authorization']);
+
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = [];
@@ -1485,9 +1649,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
-Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
- return new TransactionState($dbForProject, $authorization);
-}, ['dbForProject', 'authorization']);
+Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
+ return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
+}, ['dbForProject', 'authorization', 'getDatabasesDB']);
Http::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
@@ -1496,3 +1660,10 @@ Http::setResource('executionsRetentionCount', function (Document $project, array
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
+
+Http::setResource('embeddingAgent', function ($register) {
+ $adapter = new Ollama();
+ $adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
+ $adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
+ return new Agent($adapter);
+}, ['register']);
diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml
index 0f4df352bd..741d085445 100644
--- a/app/views/install/compose.phtml
+++ b/app/views/install/compose.phtml
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
$dbService = $this->getParam('database', 'mongodb');
-$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
+$allowedDbServices = ['mariadb', 'mongodb'];
if (!\in_array($dbService, $allowedDbServices, true)) {
$dbService = 'mongodb';
}
@@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
- image: /console:7.6.4
+ image: /console:7.8.26
restart: unless-stopped
networks:
- appwrite
diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css
index b1d8fe5089..7f253eed46 100644
--- a/app/views/install/installer/css/styles.css
+++ b/app/views/install/installer/css/styles.css
@@ -691,6 +691,19 @@ body {
transform: translateY(10px);
}
+.install-counter {
+ margin-left: auto;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ white-space: nowrap;
+ user-select: none;
+ color: var(--fgcolor-neutral-secondary);
+}
+
+.install-row[data-status='in-progress'] .install-counter:not(:empty) {
+ opacity: 1;
+}
+
.install-row-toggle {
margin-left: auto;
width: 32px;
@@ -897,6 +910,17 @@ body {
gap: var(--gap-m);
}
+.install-global-actions {
+ display: flex;
+ justify-content: center;
+ gap: var(--gap-m);
+ padding: var(--space-4) 0;
+}
+
+.install-global-actions.is-hidden {
+ display: none;
+}
+
.install-error-details .button {
align-self: center;
margin-top: 0;
diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js
index c531ecddce..4917a1bfe9 100644
--- a/app/views/install/installer/js/modules/context.js
+++ b/app/views/install/installer/js/modules/context.js
@@ -13,7 +13,9 @@
DOCKER_COMPOSE: 'docker-compose',
ENV_VARS: 'env-vars',
DOCKER_CONTAINERS: 'docker-containers',
- ACCOUNT_SETUP: 'account-setup'
+ ACCOUNT_SETUP: 'account-setup',
+ SSL_CERTIFICATE: 'ssl-certificate',
+ REDIRECT: 'redirect'
});
const STATUS = Object.freeze({
@@ -75,7 +77,7 @@
{
id: STEP_IDS.ACCOUNT_SETUP,
inProgress: 'Creating Appwrite account...',
- done: 'Appwrite account created (redirecting...)'
+ done: 'Appwrite account created'
}
]);
diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js
index 7f7b23e3fc..d066908b03 100644
--- a/app/views/install/installer/js/modules/progress.js
+++ b/app/views/install/installer/js/modules/progress.js
@@ -21,7 +21,7 @@
storeInstallId,
clearInstallId
} = window.InstallerStepsState || {};
- const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
+ const { extractHostname, isLocalHost, isIPAddress } = window.InstallerStepsValidation || {};
const { generateSecretKey } = window.InstallerStepsUI || {};
const { showToast } = window.InstallerToast || {};
@@ -111,10 +111,10 @@
return normalized.summary || 'Installation failed.';
}
if (status === STATUS.COMPLETED) return step.done;
- return step.inProgress;
+ return message || step.inProgress;
};
- const updateInstallRow = (row, step, status, message) => {
+ const updateInstallRow = (row, step, status, message, details) => {
if (!row || !step) return;
row.dataset.status = status;
row.dataset.step = step.id;
@@ -138,6 +138,15 @@
}
}
+ const counter = row.querySelector('[data-install-counter]');
+ if (counter) {
+ const started = details?.containerStarted ?? 0;
+ const total = details?.containerTotal;
+ counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
+ ? `${started}/${total}`
+ : '';
+ }
+
// Show/hide "Navigate to Console" button for account setup errors
const consoleBtn = row.querySelector('[data-install-console]');
if (consoleBtn) {
@@ -251,7 +260,7 @@
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
};
- const buildRedirectUrl = () => {
+ const buildRedirectUrl = (protocol) => {
const dataset = getBodyDataset?.() ?? {};
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
if (!rawDomain) return '';
@@ -266,22 +275,53 @@
} else if (normalizedHost === 'traefik') {
host = rawDomain.replace(hostForProtocol, 'localhost');
}
- let protocol = 'http';
- let port = httpPort;
- if (httpsPort && httpsPort !== '0' && !isLocalHost?.(normalizedHost)) {
- protocol = 'https';
- port = httpsPort;
- }
- if (!hasPort && port && ((protocol === 'http' && port !== '80') || (protocol === 'https' && port !== '443'))) {
+ const port = protocol === 'https' ? httpsPort : httpPort;
+ const defaultPort = protocol === 'https' ? '443' : '80';
+ if (!hasPort && port && port !== defaultPort) {
host = `${host}:${port}`;
}
return `${protocol}://${host}`;
};
- const redirectToApp = () => {
- const url = buildRedirectUrl();
+ const normalizeHostname = (rawDomain) => {
+ const hostname = extractHostname?.(rawDomain)?.toLowerCase?.() ?? '';
+ if (hostname === '0.0.0.0' || hostname === 'traefik') return 'localhost';
+ return hostname;
+ };
+
+ const canUseHttps = () => {
+ const dataset = getBodyDataset?.() ?? {};
+ const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
+ const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
+ if (!httpsPort || httpsPort === '0') return false;
+ const hostname = normalizeHostname(rawDomain);
+ return !isLocalHost?.(hostname) && !isIPAddress?.(hostname);
+ };
+
+ const pollCertificate = async (domain, port, maxAttempts, intervalMs) => {
+ for (let i = 0; i < maxAttempts; i++) {
+ try {
+ const response = await fetch(
+ `/install/certificate?domain=${encodeURIComponent(domain)}&port=${encodeURIComponent(port)}`,
+ { cache: 'no-store' }
+ );
+ if (response.ok) {
+ const data = await response.json();
+ if (data.ready) return true;
+ }
+ } catch {
+ // Installer server may have shut down
+ }
+ if (i < maxAttempts - 1) {
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
+ }
+ }
+ return false;
+ };
+
+ const redirectToApp = (protocol) => {
+ const url = buildRedirectUrl(protocol);
if (!url) return;
- // Fire-and-forget: tell the installer server it can shut down
fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {});
window.location.href = url;
};
@@ -318,7 +358,7 @@
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
- const normalizedEmail = (formState?.emailCertificates || '').trim();
+ const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
@@ -406,6 +446,7 @@
const initStep5 = (root) => {
if (!root) return;
+ let resolvedProtocol = 'http';
if (activeInstall?.controller) {
activeInstall.controller.abort();
@@ -497,7 +538,7 @@
if (!state) return;
const row = ensureRow(step);
if (row) {
- updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
+ updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
if (state.status === STATUS?.ERROR) {
updateInstallErrorDetails(row, {
message: state.message,
@@ -547,6 +588,9 @@
}
}
}
+ if (payload.status === STATUS.ERROR) {
+ showGlobalActions();
+ }
scheduleFallback();
};
@@ -584,6 +628,7 @@
const applySnapshot = (snapshot) => {
if (!snapshot || !snapshot.steps) return;
+ let hasErrors = false;
INSTALLATION_STEPS.forEach((step) => {
const detail = snapshot.steps[step.id];
if (!detail) return;
@@ -592,8 +637,14 @@
message: detail.message,
details: snapshot.details?.[step.id]
});
+ if (detail.status === STATUS.ERROR) {
+ hasErrors = true;
+ }
});
renderProgress();
+ if (hasErrors) {
+ showGlobalActions();
+ }
};
const checkAllCompleted = () => {
@@ -605,9 +656,7 @@
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
- notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
- setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
- });
+ startSslCheck(sessionDetails);
};
const startPolling = () => {
@@ -644,6 +693,77 @@
}
stopSyncedSpinnerRotation();
setUnloadGuard(false);
+ clearInstallLock?.();
+ };
+
+ const SSL_STEP = {
+ id: STEP_IDS.SSL_CERTIFICATE,
+ inProgress: 'Generating SSL certificate...',
+ done: 'SSL certificate verified'
+ };
+
+ const REDIRECT_STEP = {
+ id: STEP_IDS.REDIRECT,
+ inProgress: 'Redirecting to console...',
+ done: 'Redirecting to console...'
+ };
+
+ const showRedirectStep = (sessionDetails, protocol) => {
+ animatePanelHeight(() => {
+ progressState.set(REDIRECT_STEP.id, {
+ status: STATUS.IN_PROGRESS,
+ message: REDIRECT_STEP.inProgress
+ });
+ const row = ensureRow(REDIRECT_STEP);
+ if (row) {
+ updateInstallRow(row, REDIRECT_STEP, STATUS.IN_PROGRESS, REDIRECT_STEP.inProgress);
+ }
+ });
+ startSyncedSpinnerRotation(list);
+
+ notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
+ setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
+ });
+ };
+
+ const startSslCheck = (sessionDetails) => {
+ if (!canUseHttps()) {
+ showRedirectStep(sessionDetails, 'http');
+ return;
+ }
+
+ animatePanelHeight(() => {
+ progressState.set(SSL_STEP.id, {
+ status: STATUS.IN_PROGRESS,
+ message: SSL_STEP.inProgress
+ });
+ const row = ensureRow(SSL_STEP);
+ if (row) {
+ updateInstallRow(row, SSL_STEP, STATUS.IN_PROGRESS, SSL_STEP.inProgress);
+ }
+ });
+ startSyncedSpinnerRotation(list);
+
+ const dataset = getBodyDataset?.() ?? {};
+ const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
+ const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '443').trim();
+ const domain = normalizeHostname(rawDomain);
+ pollCertificate(domain, httpsPort, 15, 2000).then((ready) => {
+ stopSyncedSpinnerRotation();
+ const certMessage = ready ? SSL_STEP.done : 'Certificate not ready, continuing over HTTP';
+ animatePanelHeight(() => {
+ progressState.set(SSL_STEP.id, {
+ status: STATUS.COMPLETED,
+ message: certMessage
+ });
+ const row = ensureRow(SSL_STEP);
+ if (row) {
+ updateInstallRow(row, SSL_STEP, STATUS.COMPLETED, certMessage);
+ }
+ });
+ resolvedProtocol = ready ? 'https' : 'http';
+ showRedirectStep(sessionDetails, resolvedProtocol);
+ });
};
const startInstallStream = async (installId, options = {}) => {
@@ -746,9 +866,7 @@
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
- notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
- setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
- });
+ startSslCheck(sessionDetails);
return;
}
if (event === SSE_EVENTS.ERROR) {
@@ -792,9 +910,22 @@
}
};
+ const isSnapshotTerminal = (snapshot) => {
+ if (!snapshot?.steps) return true;
+ const stepEntries = Object.values(snapshot.steps);
+ if (stepEntries.length === 0) return true;
+ const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
+ if (hasError) return true;
+ const allCompleted = INSTALLATION_STEPS.every((step) => {
+ const detail = snapshot.steps[step.id];
+ return detail && detail.status === STATUS.COMPLETED;
+ });
+ return allCompleted;
+ };
+
const resumeInstall = async (installId) => {
const snapshot = await fetchInstallStatus(installId);
- if (!snapshot) return false;
+ if (!snapshot || isSnapshotTerminal(snapshot)) return false;
activeInstall = {
installId,
controller: new AbortController(),
@@ -857,7 +988,7 @@
const retryButton = event.target.closest('[data-install-retry]');
if (consoleButton) {
- redirectToApp();
+ redirectToApp(resolvedProtocol);
return;
}
@@ -868,6 +999,60 @@
}
});
+ const globalActions = root.querySelector('[data-install-global-actions]');
+
+ const showGlobalActions = () => {
+ if (globalActions) {
+ globalActions.classList.remove('is-hidden');
+ }
+ };
+
+ const performReset = async (hard) => {
+ const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
+
+ try {
+ const res = await fetch('/install/reset', {
+ method: 'POST',
+ headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({ installId: installId || '', hard })
+ });
+ if (hard && !res.ok) {
+ const data = await res.json().catch(() => ({}));
+ showToast?.({
+ status: 'error',
+ title: 'Reset failed',
+ description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
+ dismissible: true
+ });
+ return;
+ }
+ } catch (e) {
+ console.error('Reset request failed:', e);
+ }
+
+ clearInstallLock?.();
+ clearInstallId?.();
+ cleanupInstallFlow();
+ window.location.href = '/?step=1';
+ };
+
+ const startOverButton = root.querySelector('[data-install-start-over]');
+ if (startOverButton) {
+ startOverButton.addEventListener('click', () => performReset(false));
+ }
+
+ const hardResetButton = root.querySelector('[data-install-hard-reset]');
+ if (hardResetButton) {
+ hardResetButton.addEventListener('click', () => {
+ const confirmed = window.confirm(
+ 'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
+ );
+ if (confirmed) {
+ performReset(true);
+ }
+ });
+ }
+
// When the user switches back to this tab, check if installation
// finished while the tab was in the background.
document.addEventListener('visibilitychange', () => {
@@ -876,6 +1061,14 @@
}
});
+ const startFreshInstall = () => {
+ clearInstallId?.();
+ clearInstallLock?.();
+ const newInstallId = generateInstallId();
+ storeInstallId?.(newInstallId);
+ startInstallStream(newInstallId);
+ };
+
const lock = getInstallLock?.();
const existingInstallId = lock?.installId || getStoredInstallId?.();
if (existingInstallId) {
@@ -883,15 +1076,11 @@
if (!resumed) {
clearInstallId?.();
clearInstallLock?.();
- const newInstallId = generateInstallId();
- storeInstallId?.(newInstallId);
- startInstallStream(newInstallId);
+ window.location.href = '/?step=1';
}
});
} else {
- const newInstallId = generateInstallId();
- storeInstallId?.(newInstallId);
- startInstallStream(newInstallId);
+ startFreshInstall();
}
};
diff --git a/app/views/install/installer/js/modules/state.js b/app/views/install/installer/js/modules/state.js
index 9fcf9969a8..3c7fcd2427 100644
--- a/app/views/install/installer/js/modules/state.js
+++ b/app/views/install/installer/js/modules/state.js
@@ -7,6 +7,8 @@
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
+ const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
+ const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
const formState = {
appDomain: null,
@@ -55,13 +57,24 @@
const getInstallLock = () => {
try {
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
- if (!raw) return null;
- const parsed = JSON.parse(raw);
- if (!parsed || typeof parsed !== 'object') return null;
- return parsed;
- } catch (error) {
- return null;
- }
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (parsed && typeof parsed === 'object') return parsed;
+ }
+ } catch (error) {}
+
+ try {
+ const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (parsed && typeof parsed === 'object') {
+ sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
+ return parsed;
+ }
+ }
+ } catch (error) {}
+
+ return null;
};
const setInstallLock = (installId, payload) => {
@@ -79,6 +92,9 @@
try {
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
} catch (error) {}
+ try {
+ localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
+ } catch (error) {}
if (document.body) {
document.body.dataset.installLocked = 'true';
}
@@ -89,6 +105,9 @@
try {
sessionStorage.removeItem(INSTALL_LOCK_KEY);
} catch (error) {}
+ try {
+ localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
+ } catch (error) {}
if (document.body) {
delete document.body.dataset.installLocked;
}
@@ -121,22 +140,31 @@
const getStoredInstallId = () => {
try {
- return sessionStorage.getItem(INSTALL_ID_KEY);
- } catch (error) {
- return null;
- }
+ const val = sessionStorage.getItem(INSTALL_ID_KEY);
+ if (val) return val;
+ } catch (error) {}
+ try {
+ return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
+ } catch (error) {}
+ return null;
};
const storeInstallId = (installId) => {
try {
sessionStorage.setItem(INSTALL_ID_KEY, installId);
} catch (error) {}
+ try {
+ localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
+ } catch (error) {}
};
const clearInstallId = () => {
try {
sessionStorage.removeItem(INSTALL_ID_KEY);
} catch (error) {}
+ try {
+ localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
+ } catch (error) {}
};
window.InstallerStepsState = {
diff --git a/app/views/install/installer/js/modules/ui.js b/app/views/install/installer/js/modules/ui.js
index bde4cb7c44..a41a657602 100644
--- a/app/views/install/installer/js/modules/ui.js
+++ b/app/views/install/installer/js/modules/ui.js
@@ -240,6 +240,9 @@
if (key === 'database') {
value = toDatabaseLabel(formState?.database);
}
+ if (key === 'emailCertificates' && !value) {
+ value = formState?.accountEmail;
+ }
if (value) {
node.textContent = value;
}
diff --git a/app/views/install/installer/js/modules/validation.js b/app/views/install/installer/js/modules/validation.js
index 13ab60ef4e..daa66eb8d6 100644
--- a/app/views/install/installer/js/modules/validation.js
+++ b/app/views/install/installer/js/modules/validation.js
@@ -106,12 +106,18 @@
return LOCAL_HOSTS.has(normalized);
};
+ const isIPAddress = (host) => {
+ if (!host) return false;
+ return isValidIPv4(host) || isValidIPv6(host);
+ };
+
window.InstallerStepsValidation = {
isValidEmail,
isValidPort,
isValidPassword,
isValidHostnameInput,
extractHostname,
- isLocalHost
+ isLocalHost,
+ isIPAddress
};
})();
diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js
index 2a71d075cc..c9430b7afd 100644
--- a/app/views/install/installer/js/steps.js
+++ b/app/views/install/installer/js/steps.js
@@ -390,10 +390,7 @@
if (!parsePort(httpPort, 'HTTP')) valid = false;
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
- if (!sslEmail || !sslEmail.value.trim()) {
- setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
- valid = false;
- } else if (!isValidEmail?.(sslEmail.value.trim())) {
+ if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
diff --git a/app/views/install/installer/templates/steps/step-5.phtml b/app/views/install/installer/templates/steps/step-5.phtml
index 8fa810b259..cd5de5f4ab 100644
--- a/app/views/install/installer/templates/steps/step-5.phtml
+++ b/app/views/install/installer/templates/steps/step-5.phtml
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
+
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
+
+
+
+
+
diff --git a/app/worker.php b/app/worker.php
index 840231f16c..71446ee94f 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -220,6 +220,60 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
};
}, ['pools', 'cache', 'authorization']);
+Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
+ return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
+ $projectDocument ??= $project;
+ $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
+ $databaseType = $database->getAttribute('type', '');
+
+ // Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored
+ // in the "database" attribute. In that case, fall back to the project's database DSN.
+ if ($databaseDSN === '') {
+ $databaseDSN = $projectDocument->getAttribute('database', '');
+ }
+
+ try {
+ $databaseDSN = new DSN($databaseDSN);
+ } catch (\InvalidArgumentException) {
+ $databaseDSN = new DSN('mysql://'.$databaseDSN);
+ }
+
+ try {
+ $dsn = new DSN($projectDocument->getAttribute('database'));
+ } catch (\InvalidArgumentException) {
+ // Temporary fallback until all projects use shared tables
+ $dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
+ }
+
+ $pools = $register->get('pools');
+ $pool = $pools->get($databaseDSN->getHost());
+
+ $adapter = new DatabasePool($pool);
+ $database = new Database($adapter, $cache);
+ $database
+ ->setDatabase(APP_DATABASE)
+ ->setAuthorization($authorization);
+ $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
+
+ $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
+
+ if (\in_array($dsn->getHost(), $sharedTables, true)) {
+ $database
+ ->setSharedTables(true)
+ ->setTenant((int) $projectDocument->getSequence())
+ ->setNamespace($dsn->getParam('namespace'));
+ } else {
+ $database
+ ->setSharedTables(false)
+ ->setTenant(null)
+ ->setNamespace('_' . $projectDocument->getSequence());
+ }
+
+ $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
+ return $database;
+ };
+}, ['cache', 'register', 'project', 'authorization']);
+
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
diff --git a/composer.json b/composer.json
index 2448a55522..65838a1615 100644
--- a/composer.json
+++ b/composer.json
@@ -13,9 +13,9 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test --config pint.json",
"format": "vendor/bin/pint --config pint.json",
- "analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
+ "analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"bench": "vendor/bin/phpbench run --report=benchmark",
- "check": "./vendor/bin/phpstan analyse -c phpstan.neon",
+ "check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
},
@@ -52,7 +52,6 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
- "utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
@@ -62,6 +61,7 @@
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
+ "utopia-php/agents": "1.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
@@ -73,7 +73,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
- "utopia-php/migration": "1.7.*",
+ "utopia-php/migration": "1.8.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
@@ -84,7 +84,7 @@
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
- "utopia-php/vcs": "2.*",
+ "utopia-php/vcs": "3.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
@@ -97,12 +97,6 @@
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
},
- "repositories": [
- {
- "type": "vcs",
- "url": "https://github.com/utopia-php/database"
- }
- ],
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "*",
@@ -114,18 +108,11 @@
"czproject/git-php": "4.*",
"laravel/pint": "1.*"
},
- "repositories": [
- {
- "type": "vcs",
- "url": "https://github.com/utopia-php/database"
- }
- ],
"provide": {
"ext-phpiredis": "*"
},
"config": {
"platform": {
- "php": "8.3"
},
"allow-plugins": {
"php-http/discovery": true,
diff --git a/composer.lock b/composer.lock
index 50b317c811..7a30e2f265 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": "1404c8821e43b3fe92e06a8ed658ed26",
+ "content-hash": "f9225f2b580de0ccb796b2fb8c881384",
"packages": [
{
"name": "adhocore/jwt",
@@ -3889,38 +3889,7 @@
"Utopia\\Database\\": "src/Database"
}
},
- "autoload-dev": {
- "psr-4": {
- "Tests\\E2E\\": "tests/e2e",
- "Tests\\Unit\\": "tests/unit"
- }
- },
- "scripts": {
- "build": [
- "Composer\\Config::disableProcessTimeout",
- "docker compose build"
- ],
- "start": [
- "Composer\\Config::disableProcessTimeout",
- "docker compose up -d"
- ],
- "test": [
- "Composer\\Config::disableProcessTimeout",
- "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml"
- ],
- "lint": [
- "php -d memory_limit=2G ./vendor/bin/pint --test"
- ],
- "format": [
- "php -d memory_limit=2G ./vendor/bin/pint"
- ],
- "check": [
- "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G"
- ],
- "coverage": [
- "./vendor/bin/coverage-check ./tmp/clover.xml 90"
- ]
- },
+ "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -3933,8 +3902,8 @@
"utopia"
],
"support": {
- "source": "https://github.com/utopia-php/database/tree/5.3.17",
- "issues": "https://github.com/utopia-php/database/issues"
+ "issues": "https://github.com/utopia-php/database/issues",
+ "source": "https://github.com/utopia-php/database/tree/5.3.17"
},
"time": "2026-03-20T01:18:52+00:00"
},
@@ -4549,16 +4518,16 @@
},
{
"name": "utopia-php/migration",
- "version": "1.7.0",
+ "version": "1.8.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
- "reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
+ "reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
- "reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
+ "url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
+ "reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
"shasum": ""
},
"require": {
@@ -4598,9 +4567,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
- "source": "https://github.com/utopia-php/migration/tree/1.7.0"
+ "source": "https://github.com/utopia-php/migration/tree/1.8.3"
},
- "time": "2026-03-10T06:36:27+00:00"
+ "time": "2026-03-19T09:18:47+00:00"
},
{
"name": "utopia-php/mongo",
@@ -5247,22 +5216,23 @@
},
{
"name": "utopia-php/vcs",
- "version": "2.0.2",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
- "reference": "5769679308bad498f2777547d48ab332166c4c0b"
+ "reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
- "reference": "5769679308bad498f2777547d48ab332166c4c0b",
+ "url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
+ "reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
- "utopia-php/cache": "1.0.*"
+ "utopia-php/cache": "1.0.*",
+ "utopia-php/fetch": "0.5.*"
},
"require-dev": {
"laravel/pint": "1.*.*",
@@ -5289,9 +5259,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
- "source": "https://github.com/utopia-php/vcs/tree/2.0.2"
+ "source": "https://github.com/utopia-php/vcs/tree/3.1.0"
},
- "time": "2026-03-13T15:25:16+00:00"
+ "time": "2026-03-24T08:49:14+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5469,16 +5439,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "1.11.11",
+ "version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a"
+ "reference": "a724aa8db52f83ea35854a004837fa5ce990b736"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cfc37c85161a5515af4cd2f9885a811f51a2483a",
- "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a724aa8db52f83ea35854a004837fa5ce990b736",
+ "reference": "a724aa8db52f83ea35854a004837fa5ce990b736",
"shasum": ""
},
"require": {
@@ -5514,9 +5484,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.11.11"
+ "source": "https://github.com/appwrite/sdk-generator/tree/1.12.1"
},
- "time": "2026-03-19T16:21:03+00:00"
+ "time": "2026-03-24T05:18:43+00:00"
},
{
"name": "brianium/paratest",
@@ -8486,8 +8456,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
- "platform-overrides": {
- "php": "8.3"
- },
"plugin-api-version": "2.9.0"
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 7d64dfa867..aa2bfdd16a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -112,6 +112,8 @@ services:
condition: service_healthy
coredns:
condition: service_started
+ ollama:
+ condition: service_started
entrypoint:
- php
- -e
@@ -159,6 +161,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER_VECTORSDB
+ - _APP_DB_HOST_VECTORSDB
+ - _APP_DB_PORT_VECTORSDB
+ - _APP_DB_SCHEMA_VECTORSDB
+ - _APP_DB_USER_VECTORSDB
+ - _APP_DB_PASS_VECTORSDB
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -246,7 +254,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
- image: appwrite/console:7.5.7
+ image: appwrite/console:7.8.26
restart: unless-stopped
networks:
- appwrite
@@ -295,6 +303,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -311,6 +320,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER_VECTORSDB
+ - _APP_DB_HOST_VECTORSDB
+ - _APP_DB_PORT_VECTORSDB
+ - _APP_DB_SCHEMA_VECTORSDB
+ - _APP_DB_USER_VECTORSDB
+ - _APP_DB_PASS_VECTORSDB
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
@@ -330,6 +345,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -363,6 +379,7 @@ services:
- ${_APP_DB_HOST:-mongodb}
- request-catcher-sms
- request-catcher-webhook
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -393,6 +410,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -402,6 +420,7 @@ services:
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
+
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -458,9 +477,11 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
+
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -476,6 +497,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER_VECTORSDB
+ - _APP_DB_HOST_VECTORSDB
+ - _APP_DB_PORT_VECTORSDB
+ - _APP_DB_SCHEMA_VECTORSDB
+ - _APP_DB_USER_VECTORSDB
+ - _APP_DB_PASS_VECTORSDB
- _APP_LOGGING_CONFIG
- _APP_WORKERS_NUM
- _APP_QUEUE_NAME
@@ -497,6 +524,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -629,6 +657,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -848,6 +877,7 @@ services:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
+
depends_on:
- ${_APP_DB_HOST:-mongodb}
environment:
@@ -1044,6 +1074,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1077,6 +1108,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1107,6 +1139,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1137,6 +1170,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
+ - ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1228,7 +1262,6 @@ services:
start_period: 5s
mariadb:
- profiles: ["mariadb"]
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
@@ -1252,7 +1285,6 @@ services:
retries: 12
mongodb:
- profiles: ["mongodb"]
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
@@ -1288,32 +1320,41 @@ services:
retries: 10
start_period: 30s
-
-
postgresql:
- profiles: ["postgresql"]
- build:
- context: ./tests/resources/postgresql
- args:
- POSTGRES_VERSION: 17
+ image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
<<: *x-logging
networks:
- appwrite
volumes:
- - appwrite-postgresql:/var/lib/postgresql:rw
+ - appwrite-postgresql:/var/lib/postgresql/18/data:rw
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${_APP_DB_SCHEMA}
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
- command: "postgres"
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
+ test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
interval: 5s
timeout: 5s
- retries: 12
+ retries: 10
+ start_period: 10s
+ command: "postgres"
+
+ ollama:
+ image: appwrite/ollama:0.1.1
+ container_name: ollama
+ ports:
+ - "11434:11434"
+ restart: unless-stopped
+ environment:
+ MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma}
+ OLLAMA_KEEP_ALIVE: 24h
+ volumes:
+ - appwrite-models:/root/.ollama
+ networks:
+ - appwrite
redis:
image: redis:7.4.7-alpine
@@ -1436,3 +1477,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
+ appwrite-models:
\ No newline at end of file
diff --git a/docs/references/documentsdb/create-collection.md b/docs/references/documentsdb/create-collection.md
new file mode 100644
index 0000000000..c6293a4c38
--- /dev/null
+++ b/docs/references/documentsdb/create-collection.md
@@ -0,0 +1 @@
+Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
\ No newline at end of file
diff --git a/docs/references/documentsdb/create-document.md b/docs/references/documentsdb/create-document.md
new file mode 100644
index 0000000000..197744b4a0
--- /dev/null
+++ b/docs/references/documentsdb/create-document.md
@@ -0,0 +1 @@
+Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
\ No newline at end of file
diff --git a/docs/references/documentsdb/create-documents.md b/docs/references/documentsdb/create-documents.md
new file mode 100644
index 0000000000..9f4a4a1396
--- /dev/null
+++ b/docs/references/documentsdb/create-documents.md
@@ -0,0 +1 @@
+Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
\ No newline at end of file
diff --git a/docs/references/documentsdb/create-index.md b/docs/references/documentsdb/create-index.md
new file mode 100644
index 0000000000..164b754161
--- /dev/null
+++ b/docs/references/documentsdb/create-index.md
@@ -0,0 +1,2 @@
+Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.
+Attributes can be `key`, `fulltext`, and `unique`.
\ No newline at end of file
diff --git a/docs/references/documentsdb/create.md b/docs/references/documentsdb/create.md
new file mode 100644
index 0000000000..b608485341
--- /dev/null
+++ b/docs/references/documentsdb/create.md
@@ -0,0 +1 @@
+Create a new Database.
diff --git a/docs/references/documentsdb/decrement-document-attribute.md b/docs/references/documentsdb/decrement-document-attribute.md
new file mode 100644
index 0000000000..b7b32d6148
--- /dev/null
+++ b/docs/references/documentsdb/decrement-document-attribute.md
@@ -0,0 +1 @@
+Decrement a specific column of a row by a given value.
\ No newline at end of file
diff --git a/docs/references/documentsdb/delete-collection.md b/docs/references/documentsdb/delete-collection.md
new file mode 100644
index 0000000000..90f7aa6aa5
--- /dev/null
+++ b/docs/references/documentsdb/delete-collection.md
@@ -0,0 +1 @@
+Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
\ No newline at end of file
diff --git a/docs/references/documentsdb/delete-document.md b/docs/references/documentsdb/delete-document.md
new file mode 100644
index 0000000000..36fbf6802d
--- /dev/null
+++ b/docs/references/documentsdb/delete-document.md
@@ -0,0 +1 @@
+Delete a document by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/delete-documents.md b/docs/references/documentsdb/delete-documents.md
new file mode 100644
index 0000000000..a7b05503de
--- /dev/null
+++ b/docs/references/documentsdb/delete-documents.md
@@ -0,0 +1 @@
+Bulk delete documents using queries, if no queries are passed then all documents are deleted.
\ No newline at end of file
diff --git a/docs/references/documentsdb/delete-index.md b/docs/references/documentsdb/delete-index.md
new file mode 100644
index 0000000000..c5b8f49e5f
--- /dev/null
+++ b/docs/references/documentsdb/delete-index.md
@@ -0,0 +1 @@
+Delete an index.
\ No newline at end of file
diff --git a/docs/references/documentsdb/delete.md b/docs/references/documentsdb/delete.md
new file mode 100644
index 0000000000..605fa290d3
--- /dev/null
+++ b/docs/references/documentsdb/delete.md
@@ -0,0 +1 @@
+Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-collection-logs.md b/docs/references/documentsdb/get-collection-logs.md
new file mode 100644
index 0000000000..8578cef03c
--- /dev/null
+++ b/docs/references/documentsdb/get-collection-logs.md
@@ -0,0 +1 @@
+Get the collection activity logs list by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-collection-usage.md b/docs/references/documentsdb/get-collection-usage.md
new file mode 100644
index 0000000000..48682a075f
--- /dev/null
+++ b/docs/references/documentsdb/get-collection-usage.md
@@ -0,0 +1 @@
+Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-collection.md b/docs/references/documentsdb/get-collection.md
new file mode 100644
index 0000000000..97b39e8474
--- /dev/null
+++ b/docs/references/documentsdb/get-collection.md
@@ -0,0 +1 @@
+Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-database-usage.md b/docs/references/documentsdb/get-database-usage.md
new file mode 100644
index 0000000000..2c2628a464
--- /dev/null
+++ b/docs/references/documentsdb/get-database-usage.md
@@ -0,0 +1 @@
+Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-document-logs.md b/docs/references/documentsdb/get-document-logs.md
new file mode 100644
index 0000000000..9b96df5ad4
--- /dev/null
+++ b/docs/references/documentsdb/get-document-logs.md
@@ -0,0 +1 @@
+Get the document activity logs list by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-document.md b/docs/references/documentsdb/get-document.md
new file mode 100644
index 0000000000..4e4d76bec0
--- /dev/null
+++ b/docs/references/documentsdb/get-document.md
@@ -0,0 +1 @@
+Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-index.md b/docs/references/documentsdb/get-index.md
new file mode 100644
index 0000000000..cdea5b4f27
--- /dev/null
+++ b/docs/references/documentsdb/get-index.md
@@ -0,0 +1 @@
+Get index by ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get-logs.md b/docs/references/documentsdb/get-logs.md
new file mode 100644
index 0000000000..8e49da4603
--- /dev/null
+++ b/docs/references/documentsdb/get-logs.md
@@ -0,0 +1 @@
+Get the database activity logs list by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/get.md b/docs/references/documentsdb/get.md
new file mode 100644
index 0000000000..24183f6f6b
--- /dev/null
+++ b/docs/references/documentsdb/get.md
@@ -0,0 +1 @@
+Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
\ No newline at end of file
diff --git a/docs/references/documentsdb/increment-document-attribute.md b/docs/references/documentsdb/increment-document-attribute.md
new file mode 100644
index 0000000000..7a19b3fbc7
--- /dev/null
+++ b/docs/references/documentsdb/increment-document-attribute.md
@@ -0,0 +1 @@
+Increment a specific column of a row by a given value.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list-attributes.md b/docs/references/documentsdb/list-attributes.md
new file mode 100644
index 0000000000..72ad6d727f
--- /dev/null
+++ b/docs/references/documentsdb/list-attributes.md
@@ -0,0 +1 @@
+List attributes in the collection.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list-collections.md b/docs/references/documentsdb/list-collections.md
new file mode 100644
index 0000000000..e437674915
--- /dev/null
+++ b/docs/references/documentsdb/list-collections.md
@@ -0,0 +1 @@
+Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list-documents.md b/docs/references/documentsdb/list-documents.md
new file mode 100644
index 0000000000..4e2ae91792
--- /dev/null
+++ b/docs/references/documentsdb/list-documents.md
@@ -0,0 +1 @@
+Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list-indexes.md b/docs/references/documentsdb/list-indexes.md
new file mode 100644
index 0000000000..a8c687fb2b
--- /dev/null
+++ b/docs/references/documentsdb/list-indexes.md
@@ -0,0 +1 @@
+List indexes in the collection.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list-usage.md b/docs/references/documentsdb/list-usage.md
new file mode 100644
index 0000000000..a88e76680e
--- /dev/null
+++ b/docs/references/documentsdb/list-usage.md
@@ -0,0 +1 @@
+List usage metrics and statistics for all databases in the project. You can view the total number of databases, collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
\ No newline at end of file
diff --git a/docs/references/documentsdb/list.md b/docs/references/documentsdb/list.md
new file mode 100644
index 0000000000..d93fb9d7a8
--- /dev/null
+++ b/docs/references/documentsdb/list.md
@@ -0,0 +1 @@
+Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
\ No newline at end of file
diff --git a/docs/references/documentsdb/update-collection.md b/docs/references/documentsdb/update-collection.md
new file mode 100644
index 0000000000..b8f6bef997
--- /dev/null
+++ b/docs/references/documentsdb/update-collection.md
@@ -0,0 +1 @@
+Update a collection by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/update-document.md b/docs/references/documentsdb/update-document.md
new file mode 100644
index 0000000000..526f3971d1
--- /dev/null
+++ b/docs/references/documentsdb/update-document.md
@@ -0,0 +1 @@
+Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
\ No newline at end of file
diff --git a/docs/references/documentsdb/update-documents.md b/docs/references/documentsdb/update-documents.md
new file mode 100644
index 0000000000..5f560c6435
--- /dev/null
+++ b/docs/references/documentsdb/update-documents.md
@@ -0,0 +1 @@
+Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated.
\ No newline at end of file
diff --git a/docs/references/documentsdb/update.md b/docs/references/documentsdb/update.md
new file mode 100644
index 0000000000..4e99bf2e07
--- /dev/null
+++ b/docs/references/documentsdb/update.md
@@ -0,0 +1 @@
+Update a database by its unique ID.
\ No newline at end of file
diff --git a/docs/references/documentsdb/upsert-document.md b/docs/references/documentsdb/upsert-document.md
new file mode 100644
index 0000000000..f1b68d13d5
--- /dev/null
+++ b/docs/references/documentsdb/upsert-document.md
@@ -0,0 +1 @@
+Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
\ No newline at end of file
diff --git a/docs/references/documentsdb/upsert-documents.md b/docs/references/documentsdb/upsert-documents.md
new file mode 100644
index 0000000000..4feb473076
--- /dev/null
+++ b/docs/references/documentsdb/upsert-documents.md
@@ -0,0 +1 @@
+Create or update Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
diff --git a/mongo-init-replicaset.sh b/mongo-init-replicaset.sh
old mode 100755
new mode 100644
diff --git a/mongo-init.js b/mongo-init.js
index edff6cc499..bc06ba5b23 100644
--- a/mongo-init.js
+++ b/mongo-init.js
@@ -15,4 +15,4 @@ adminDb.createUser({
roles: [
{ role: 'readWrite', db: database }
]
-});
+});
\ No newline at end of file
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 979deae17e..eca26955f0 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -210,12 +210,6 @@ parameters:
count: 1
path: src/Appwrite/Auth/Validator/PersonalData.php
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: src/Appwrite/Databases/TransactionState.php
-
-
message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#'
identifier: phpDoc.parseError
@@ -1217,55 +1211,11 @@ parameters:
identifier: return.phpDocType
count: 1
path: src/Appwrite/Utopia/Response/Model/User.php
-
- -
- message: '#^Attribute class Tests\\E2E\\General\\Retry does not exist\.$#'
- identifier: attribute.notFound
- count: 1
- path: tests/e2e/General/UsageTest.php
-
-
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
identifier: staticClassAccess.privateProperty
count: 4
path: tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/Databases/LegacyConsoleClientTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/Databases/LegacyConsoleClientTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/Databases/LegacyCustomClientTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/Databases/LegacyCustomClientTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/Databases/LegacyCustomServerTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/Databases/LegacyCustomServerTest.php
-
-
message: '#^Call to an undefined method Tests\\E2E\\Services\\Databases\\Permissions\\LegacyPermissionsGuestTest\:\:getIndexUrl\(\)\.$#'
identifier: method.notFound
@@ -1649,43 +1599,6 @@ parameters:
identifier: method.notFound
count: 1
path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/TablesDB/TablesDBConsoleClientTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/TablesDB/TablesDBConsoleClientTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/TablesDB/TablesDBCustomClientTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/TablesDB/TablesDBCustomClientTest.php
-
- -
- message: '#^Variable \$library might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: tests/e2e/Services/TablesDB/TablesDBCustomServerTest.php
-
- -
- message: '#^Variable \$person might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: tests/e2e/Services/TablesDB/TablesDBCustomServerTest.php
-
-
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
identifier: staticClassAccess.privateProperty
diff --git a/phpunit.xml b/phpunit.xml
index 9ccbaf47cc..9748c5a5c8 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -37,6 +37,7 @@
./tests/e2e/Services/ProjectWebhooks
./tests/e2e/Services/Messaging
./tests/e2e/Services/Migrations
+ ./tests/e2e/Services/Project
./tests/e2e/Services/Functions/FunctionsBase.php
./tests/e2e/Services/Functions/FunctionsCustomServerTest.php
./tests/e2e/Services/Functions/FunctionsCustomClientTest.php
diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php
index 8e098774e6..71bd8799c7 100644
--- a/src/Appwrite/Databases/TransactionState.php
+++ b/src/Appwrite/Databases/TransactionState.php
@@ -21,17 +21,23 @@ class TransactionState
{
private Database $dbForProject;
private Authorization $authorization;
- /** @var Authorization $authorization */
- public function __construct(Database $dbForProject, Authorization $authorization)
+ /**
+ * @var callable(Document $database): Database
+ */
+ private mixed $getDatabasesDB;
+
+ public function __construct(Database $dbForProject, Authorization $authorization, callable $getDatabasesDB)
{
$this->dbForProject = $dbForProject;
$this->authorization = $authorization;
+ $this->getDatabasesDB = $getDatabasesDB;
}
/**
* Get a document with transaction-aware logic
*
+ * @param Document $database Target database document
* @param string $collectionId Collection ID
* @param string $documentId Document ID
* @param string|null $transactionId Optional transaction ID
@@ -42,13 +48,15 @@ class TransactionState
* @throws Timeout
*/
public function getDocument(
+ Document $database,
string $collectionId,
string $documentId,
?string $transactionId = null,
array $queries = []
): Document {
+ $dbForDatabases = ($this->getDatabasesDB)($database);
if ($transactionId === null) {
- return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
+ return $dbForDatabases->getDocument($collectionId, $documentId, $queries);
}
$state = $this->getTransactionState($transactionId);
@@ -66,7 +74,7 @@ class TransactionState
if ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
// Merge with committed version
- $committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries);
+ $committedDoc = $dbForDatabases->getDocument($collectionId, $documentId, $queries);
if (!$committedDoc->isEmpty()) {
foreach ($docState['document']->getAttributes() as $key => $value) {
if ($key !== '$id') {
@@ -80,13 +88,13 @@ class TransactionState
}
}
}
-
- return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
+ return $dbForDatabases->getDocument($collectionId, $documentId, $queries);
}
/**
* List documents with transaction-aware logic
*
+ * @param Document $database Target database document
* @param string $collectionId Collection ID
* @param string|null $transactionId Optional transaction ID
* @param array $queries Optional query filters
@@ -96,17 +104,19 @@ class TransactionState
* @throws Timeout
*/
public function listDocuments(
+ Document $database,
string $collectionId,
?string $transactionId = null,
array $queries = []
): array {
+ $dbForDatabases = ($this->getDatabasesDB)($database);
// If no transaction, use normal database retrieval
if ($transactionId === null) {
- return $this->dbForProject->find($collectionId, $queries);
+ return $dbForDatabases->find($collectionId, $queries);
}
$state = $this->getTransactionState($transactionId);
- $committedDocs = $this->dbForProject->find($collectionId, $queries);
+ $committedDocs = $dbForDatabases->find($collectionId, $queries);
$documentMap = [];
// Build map of committed documents
@@ -147,6 +157,7 @@ class TransactionState
/**
* Count documents with transaction-aware logic
*
+ * @param Document $database Target database document
* @param string $collectionId Collection ID
* @param string|null $transactionId Optional transaction ID
* @param array $queries Optional query filters
@@ -156,23 +167,23 @@ class TransactionState
* @throws Timeout
*/
public function countDocuments(
+ Document $database,
string $collectionId,
?string $transactionId = null,
array $queries = []
): int {
+ $dbForDatabases = ($this->getDatabasesDB)($database);
if ($transactionId === null) {
- return $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
+ return $dbForDatabases->count($collectionId, $queries, APP_LIMIT_COUNT);
}
$state = $this->getTransactionState($transactionId);
-
- $baseCount = $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
+ $baseCount = $dbForDatabases->count($collectionId, $queries, APP_LIMIT_COUNT);
if (!isset($state[$collectionId])) {
return $baseCount;
}
-
- $committedDocs = $this->dbForProject->find($collectionId, $queries);
+ $committedDocs = $dbForDatabases->find($collectionId, $queries);
$committedDocIds = [];
foreach ($committedDocs as $doc) {
$committedDocIds[$doc->getId()] = true;
@@ -214,17 +225,19 @@ class TransactionState
/**
* Check if a document exists with transaction-aware logic
*
+ * @param Document $database Target database document
* @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(
+ Document $database,
string $collectionId,
string $documentId,
?string $transactionId = null
): bool {
- $doc = $this->getDocument($collectionId, $documentId, $transactionId);
+ $doc = $this->getDocument($database, $collectionId, $documentId, $transactionId);
return !$doc->isEmpty();
}
diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php
index ba633b4478..bf6339f8a0 100644
--- a/src/Appwrite/Event/Event.php
+++ b/src/Appwrite/Event/Event.php
@@ -519,6 +519,7 @@ class Event
* @param string $pattern
* @param array $params
* @param ?Document $database
+ * @param ?Document $database
* @return array
* @throws \InvalidArgumentException
*/
@@ -533,7 +534,7 @@ class Event
$parsed = self::parseEventPattern($pattern);
// to switch the resource types from databases to the required prefix
// eg; all databases events get fired with databases. prefix which mainly depicts legacy type
- // so a projection from databases to the actual prefix
+ // so a projection from databases to the actual prefix(documentsdb, vectorsdb,etc)
if ((str_contains($pattern, 'databases.') && $database && $database->getAttribute('type') !== 'legacy')) {
$parsed = self::getDatabaseTypeEvents($database, $parsed);
}
@@ -695,7 +696,6 @@ class Event
)
) {
$pairedEvents = [];
-
foreach ($events as $event) {
$pairedEvents[] = $event;
// tablesdb needs databases event with tables and collections
@@ -745,6 +745,13 @@ class Event
'attributes' => 'columns',
];
break;
+ case 'documentsdb':
+ case 'vectorsdb':
+ // sending the type itself(eg: documentsdb, vectorsdb)
+ $eventMap = [
+ 'databases' => $database->getAttribute('type')
+ ];
+ break;
}
foreach ($event as $eventKey => $eventValue) {
if (isset($eventMap[$eventValue])) {
diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php
index 419863191e..747fd786f9 100644
--- a/src/Appwrite/Event/Realtime.php
+++ b/src/Appwrite/Event/Realtime.php
@@ -4,6 +4,7 @@ namespace Appwrite\Event;
use Appwrite\Messaging\Adapter;
use Appwrite\Messaging\Adapter\Realtime as RealtimeAdapter;
+use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception;
@@ -96,17 +97,21 @@ class Realtime extends Event
: [$target['projectId'] ?? $this->getProject()->getId()];
foreach ($projectIds as $projectId) {
- $this->realtime->send(
- projectId: $projectId,
- payload: $this->getRealtimePayload(),
- events: $allEvents,
- channels: $target['channels'],
- roles: $target['roles'],
- options: [
- 'permissionsChanged' => $target['permissionsChanged'],
- 'userId' => $this->getParam('userId')
- ]
- );
+ try {
+ $this->realtime->send(
+ projectId: $projectId,
+ payload: $this->getRealtimePayload(),
+ events: $allEvents,
+ channels: $target['channels'],
+ roles: $target['roles'],
+ options: [
+ 'permissionsChanged' => $target['permissionsChanged'],
+ 'userId' => $this->getParam('userId')
+ ]
+ );
+ } catch (\Exception $e) {
+ Console::error('Realtime send failed: '.$e->getMessage());
+ }
}
return true;
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index a54edf7074..f7c76d3800 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -340,6 +340,7 @@ class Exception extends \Exception
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';
+ public const string MIGRATION_DATABASE_TYPE_UNSUPPORTED = 'migration_database_type_unsupported';
/** Realtime */
public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php
index 85ae4fde25..7a2b6fe19a 100644
--- a/src/Appwrite/Messaging/Adapter/Realtime.php
+++ b/src/Appwrite/Messaging/Adapter/Realtime.php
@@ -492,6 +492,8 @@ class Realtime extends MessagingAdapter
break;
case 'databases':
case 'tablesdb':
+ case 'documentsdb':
+ case 'vectorsdb':
$resource = $parts[4] ?? '';
if (in_array($resource, ['columns', 'attributes', 'indexes'])) {
$channels[] = 'console';
@@ -511,12 +513,20 @@ class Realtime extends MessagingAdapter
$resourceId = $tableId ?: $collectionId;
$channels = [];
- // sending legacy + tablesdb events to both legacy and tablesdb
- $channels = array_values(array_unique(array_merge(
- self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
- self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
- self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
- )));
+ switch ($parts[0]) {
+ case 'databases':
+ case 'tablesdb':
+ // sending legacy + tablesdb events to both legacy and tablesdb
+ $channels = array_values(array_unique(array_merge(
+ self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
+ self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
+ self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
+ )));
+ break;
+ default:
+ // only prefixed events
+ $channels = array_values(self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()));
+ }
$roles = $collection->getAttribute('documentSecurity', false)
? \array_merge($collection->getRead(), $payload->getRead())
@@ -582,6 +592,7 @@ class Realtime extends MessagingAdapter
* @param string $resourceId The collection/table ID
* @param string $payloadId The document/row ID
* @param string $prefixOverride Override the channel prefix when different API types share the same terminology but need different prefixes
+ * (e.g., 'databases' and 'documentsdb' use same terminology but need different prefixes)
* @return array Array of channel names
*/
private static function getDatabaseChannels(
@@ -615,6 +626,13 @@ class Realtime extends MessagingAdapter
$channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows.{$payloadId}";
break;
+ case 'documentsdb':
+ case 'vectorsdb':
+ $channels[] = 'documents';
+ $channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
+ $channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
+ break;
+
default:
$basePrefix = 'databases';
$channels[] = 'documents';
@@ -623,6 +641,7 @@ class Realtime extends MessagingAdapter
break;
}
+
return $channels;
}
}
diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php
index 77b9c4d1dd..06312d9cb2 100644
--- a/src/Appwrite/Platform/Appwrite.php
+++ b/src/Appwrite/Platform/Appwrite.php
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
+use Appwrite\Platform\Modules\Project;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
@@ -38,5 +39,6 @@ class Appwrite extends Platform
$this->addModule(new Storage\Module());
$this->addModule(new VCS\Module());
$this->addModule(new Webhooks\Module());
+ $this->addModule(new Project\Module());
}
}
diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Certificate/Get.php b/src/Appwrite/Platform/Installer/Http/Installer/Certificate/Get.php
new file mode 100644
index 0000000000..ab0037f4b2
--- /dev/null
+++ b/src/Appwrite/Platform/Installer/Http/Installer/Certificate/Get.php
@@ -0,0 +1,91 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/install/certificate')
+ ->desc('Check if SSL certificate is ready for a domain')
+ ->param('domain', '', new AppDomain(), 'Domain to check')
+ ->param('port', 443, new Range(1, 65535), 'HTTPS port to check', true)
+ ->inject('response')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $domain, int $port, Response $response): void
+ {
+ $domain = trim($domain);
+ if ($domain === '') {
+ $response->json(['ready' => false]);
+ return;
+ }
+
+ $ready = $this->checkHttps($domain, $port);
+ $response->json(['ready' => $ready]);
+ }
+
+ private function checkHttps(string $domain, int $port): bool
+ {
+ $gateway = $this->getDockerGateway();
+
+ $ch = curl_init();
+ $options = [
+ CURLOPT_URL => 'https://' . $domain . ':' . $port . '/',
+ CURLOPT_NOBODY => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
+ CURLOPT_TIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
+ ];
+
+ if ($gateway !== '') {
+ $options[CURLOPT_RESOLVE] = [$domain . ':' . $port . ':' . $gateway];
+ }
+
+ curl_setopt_array($ch, $options);
+ curl_exec($ch);
+ $errno = curl_errno($ch);
+ curl_close($ch);
+
+ return $errno === 0;
+ }
+
+ private function getDockerGateway(): string
+ {
+ $route = @file_get_contents('/proc/net/route');
+ if ($route === false) {
+ return '';
+ }
+
+ foreach (explode("\n", $route) as $line) {
+ $fields = preg_split('/\s+/', trim($line));
+ if (isset($fields[1]) && $fields[1] === '00000000' && isset($fields[2])) {
+ $hex = $fields[2];
+ if (strlen($hex) !== 8) {
+ continue;
+ }
+ $ip = long2ip((int) hexdec($hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]));
+ return $ip;
+ }
+ }
+
+ return '';
+ }
+}
diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Complete.php b/src/Appwrite/Platform/Installer/Http/Installer/Complete.php
index 92a00651fe..69f7d4b072 100644
--- a/src/Appwrite/Platform/Installer/Http/Installer/Complete.php
+++ b/src/Appwrite/Platform/Installer/Http/Installer/Complete.php
@@ -48,9 +48,10 @@ class Complete extends Action
@touch(Server::INSTALLER_COMPLETE_FILE);
- if (!$sessionSecret && $installId !== '') {
- $data = $state->readProgressFile($installId);
- $details = $data['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
+ $progressData = ($installId !== '') ? $state->readProgressFile($installId) : [];
+
+ if (!$sessionSecret) {
+ $details = $progressData['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
if (!empty($details['sessionSecret'])) {
$sessionSecret = $details['sessionSecret'];
$sessionId = $sessionId ?: ($details['sessionId'] ?? '');
@@ -68,8 +69,11 @@ class Complete extends Action
$expires = $timestamp;
}
}
- $response->addCookie('a_session_console', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
- $response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
+ $appDomain = $progressData['payload']['appDomain'] ?? '';
+ $cookieDomain = $this->buildCookieDomain($appDomain ?: $request->getHostname());
+
+ $response->addCookie('a_session_console', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
+ $response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
if ($sessionId) {
$response->addHeader('X-Appwrite-Session', $sessionId);
}
@@ -79,4 +83,42 @@ class Complete extends Action
$response->json(['success' => true]);
}
+
+ /**
+ * Compute the cookie domain to match Appwrite's convention in general.php.
+ *
+ * For localhost and IP addresses the domain is left empty (host-only cookie).
+ * For real hostnames, the domain is prefixed with a dot so the cookie matches
+ * Appwrite's default `'.' . $request->getHostname()` behaviour and lives in
+ * the same cookie-jar slot — preventing stale ghost cookies after logout.
+ */
+ private function buildCookieDomain(string $raw): string
+ {
+ $hostname = $this->extractHostname($raw);
+ if ($hostname === '' || $hostname === 'localhost' || $hostname === '0.0.0.0' || $hostname === 'traefik') {
+ return '';
+ }
+ if (filter_var($hostname, FILTER_VALIDATE_IP) !== false) {
+ return '';
+ }
+ return '.' . $hostname;
+ }
+
+ /**
+ * Extract the bare hostname from an appDomain value, stripping any port
+ * suffix or IPv6 bracket notation.
+ */
+ private function extractHostname(string $domain): string
+ {
+ $domain = trim($domain);
+ if ($domain === '') {
+ return '';
+ }
+ if (str_starts_with($domain, '[')) {
+ $end = strpos($domain, ']');
+ return $end !== false ? substr($domain, 1, $end - 1) : '';
+ }
+ $parts = explode(':', $domain);
+ return count($parts) <= 2 ? strtolower($parts[0]) : strtolower($domain);
+ }
}
diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Install.php b/src/Appwrite/Platform/Installer/Http/Installer/Install.php
index 0b2fa17c0d..e29222a703 100644
--- a/src/Appwrite/Platform/Installer/Http/Installer/Install.php
+++ b/src/Appwrite/Platform/Installer/Http/Installer/Install.php
@@ -35,7 +35,7 @@ class Install extends Action
->param('appDomain', '', new AppDomain(), 'Application domain (hostname, IP, or bracket IPv6 with optional port)')
->param('httpPort', 80, new Range(1, 65535), 'HTTP port')
->param('httpsPort', 443, new Range(1, 65535), 'HTTPS port')
- ->param('emailCertificates', '', new Email(), 'Email for SSL certificates')
+ ->param('emailCertificates', '', new Email(allowEmpty: true), 'Email for SSL certificates', true)
->param('opensslKey', '', new Text(64, 0), 'Secret API key', true)
->param('assistantOpenAIKey', '', new Text(256, 0), 'OpenAI API key for assistant', true)
->param('accountEmail', '', new Email(allowEmpty: true), 'Account email address', true)
@@ -90,6 +90,9 @@ class Install extends Action
$appDomain = trim($appDomain);
$emailCertificates = trim($emailCertificates);
+ if ($emailCertificates === '') {
+ $emailCertificates = trim($accountEmail);
+ }
$opensslKey = trim($opensslKey);
$assistantOpenAIKey = trim($assistantOpenAIKey);
@@ -140,6 +143,8 @@ class Install extends Action
@unlink(Server::INSTALLER_COMPLETE_FILE);
+ $state->clearStaleLockIfNeeded();
+
try {
$lockResult = $state->reserveGlobalLock($installId);
} catch (\Throwable $e) {
@@ -175,15 +180,23 @@ class Install extends Action
if (file_exists($existingPath)) {
$existing = $state->readProgressFile($installId);
if (!empty($existing['steps']) && $retryStep === null) {
- $state->updateGlobalLock($installId, Server::STATUS_ERROR);
- if ($wantsStream) {
- $this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
- $swooleResponse->end();
+ $previousHadError = isset($existing['error']);
+ $allCompleted = !$previousHadError && $this->allStepsCompleted($existing['steps']);
+
+ if ($previousHadError || $allCompleted) {
+ @unlink($existingPath);
+ $existing = null;
} else {
- $response->setStatusCode(Response::STATUS_CODE_CONFLICT);
- $response->json(['success' => false, 'message' => 'Installation already started']);
+ $state->updateGlobalLock($installId, Server::STATUS_ERROR);
+ if ($wantsStream) {
+ $this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
+ $swooleResponse->end();
+ } else {
+ $response->setStatusCode(Response::STATUS_CODE_CONFLICT);
+ $response->json(['success' => false, 'message' => 'Installation already started']);
+ }
+ return;
}
- return;
}
}
@@ -207,7 +220,8 @@ class Install extends Action
'_APP_ASSISTANT_OPENAI_API_KEY' => $assistantOpenAIKey,
];
- if ($this->hasPayload($existing)) {
+ $previousHadError = is_array($existing) && isset($existing['error']);
+ if ($this->hasPayload($existing) && !$previousHadError) {
$stored = $existing['payload'];
$inputValues = [
'httpPort' => (string) $httpPort,
@@ -368,8 +382,6 @@ class Install extends Action
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
}
- @unlink(Server::INSTALLER_CONFIG_FILE);
-
if ($wantsStream) {
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, [
'message' => $e->getMessage(),
@@ -392,6 +404,16 @@ class Install extends Action
return is_array($data) && isset($data['payload']) && is_array($data['payload']);
}
+ private function allStepsCompleted(array $steps): bool
+ {
+ foreach ($steps as $step) {
+ if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private function deriveNameFromEmail(string $email): string
{
$parts = explode('@', $email);
diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Reset.php b/src/Appwrite/Platform/Installer/Http/Installer/Reset.php
new file mode 100644
index 0000000000..8e5b877473
--- /dev/null
+++ b/src/Appwrite/Platform/Installer/Http/Installer/Reset.php
@@ -0,0 +1,110 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/install/reset')
+ ->desc('Reset installation state')
+ ->param('installId', '', new Text(64, 0), 'Installation ID', true)
+ ->param('hard', false, new Boolean(true), 'Remove all data including volumes and config files', true)
+ ->inject('request')
+ ->inject('response')
+ ->inject('installerState')
+ ->inject('installerConfig')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $installId, bool $hard, Request $request, Response $response, State $state, Config $config): void
+ {
+ if (!Validate::validateCsrf($request)) {
+ $response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
+ $response->json(['success' => false, 'message' => 'Invalid CSRF token']);
+ return;
+ }
+
+ $installId = $state->sanitizeInstallId($installId);
+
+ if ($installId !== '') {
+ @unlink($state->progressFilePath($installId));
+ $state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
+ }
+
+ // Use direct clearStaleLock (not throttled) since reset is an
+ // explicit user action that should guarantee all stale state is gone.
+ $state->clearStaleLock();
+
+ if ($hard) {
+ $error = $this->performHardReset($config);
+ if ($error !== null) {
+ $response->setStatusCode(Response::STATUS_CODE_INTERNAL_SERVER_ERROR);
+ $response->json(['success' => false, 'message' => $error]);
+ return;
+ }
+ }
+
+ $response->json(['success' => true]);
+ }
+
+ private function performHardReset(Config $config): ?string
+ {
+ $isLocal = $config->isLocal();
+ $composeFileName = $isLocal ? 'docker-compose.web-installer.yml' : 'docker-compose.yml';
+ $envFileName = $isLocal ? '.env.web-installer' : '.env';
+ $path = $isLocal ? '/usr/src/code' : '/usr/src/code/appwrite';
+
+ $composeFile = $path . '/' . $composeFileName;
+
+ if (file_exists($composeFile)) {
+ $command = array_map(escapeshellarg(...), [
+ 'docker', 'compose',
+ '-f', $composeFile,
+ ...($isLocal ? ['--project-name', 'appwrite'] : []),
+ '--project-directory', $path,
+ 'down', '-v', '--remove-orphans',
+ ]);
+
+ $output = [];
+ @exec(implode(' ', $command) . ' 2>&1', $output, $exitCode);
+
+ if ($exitCode !== 0) {
+ return 'Failed to stop containers: ' . trim(implode("\n", $output));
+ }
+
+ @unlink($composeFile);
+ }
+
+ $envFile = $path . '/' . $envFileName;
+ if (file_exists($envFile)) {
+ @unlink($envFile);
+ }
+
+ @unlink(Server::INSTALLER_CONFIG_FILE);
+ @unlink(Server::INSTALLER_LOCK_FILE);
+
+ $tempDir = sys_get_temp_dir();
+ foreach ((array) glob($tempDir . '/appwrite-install-*.json') as $file) {
+ @unlink($file);
+ }
+
+ return null;
+ }
+}
diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Status.php b/src/Appwrite/Platform/Installer/Http/Installer/Status.php
index e53a501f4c..d6ffa64c8f 100644
--- a/src/Appwrite/Platform/Installer/Http/Installer/Status.php
+++ b/src/Appwrite/Platform/Installer/Http/Installer/Status.php
@@ -28,6 +28,8 @@ class Status extends Action
public function action(string $installId, Response $response, State $state): void
{
+ $state->clearStaleLockIfNeeded();
+
$installId = $state->sanitizeInstallId($installId);
if ($installId === '') {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
diff --git a/src/Appwrite/Platform/Installer/Runtime/State.php b/src/Appwrite/Platform/Installer/Runtime/State.php
index 5552eb5632..75efd7027c 100644
--- a/src/Appwrite/Platform/Installer/Runtime/State.php
+++ b/src/Appwrite/Platform/Installer/Runtime/State.php
@@ -13,13 +13,15 @@ class State
private const string PATTERN_IPV6_WITH_PORT = '/^\[(.+)](?::(\d+))?$/';
private const int CONFIG_FILE_PERMISSION = 0600;
- private const int GLOBAL_LOCK_TIMEOUT_SECONDS = 3600;
+ private const int GLOBAL_LOCK_TIMEOUT_SECONDS = 300;
+ private const int STALE_LOCK_CHECK_INTERVAL_SECONDS = 30;
private const int PORT_MIN = 1;
private const int PORT_MAX = 65535;
private array $paths;
private bool $bootstrapped = false;
+ private int $lastStaleLockClearAt = 0;
public function __construct(array $paths)
{
@@ -254,6 +256,16 @@ class State
}
}
+ public function clearStaleLockIfNeeded(): void
+ {
+ $now = time();
+ if ($now - $this->lastStaleLockClearAt < self::STALE_LOCK_CHECK_INTERVAL_SECONDS) {
+ return;
+ }
+ $this->lastStaleLockClearAt = $now;
+ $this->clearStaleLock();
+ }
+
public function reserveGlobalLock(string $installId): string
{
return (string) $this->withGlobalLock(function ($handle, $lock) use ($installId) {
diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php
index f36c270553..17edfaac72 100644
--- a/src/Appwrite/Platform/Installer/Server.php
+++ b/src/Appwrite/Platform/Installer/Server.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Installer;
use Appwrite\Platform\Installer\Http\Installer\Error;
+use Appwrite\Platform\Installer\Runtime\Config;
use Appwrite\Platform\Installer\Runtime\State;
use Swoole\Http\Server as SwooleServer;
use Utopia\Http\Adapter\Swoole\Request;
@@ -27,6 +28,7 @@ class Server
public const string STEP_DOCKER_COMPOSE = 'docker-compose';
public const string STEP_DOCKER_CONTAINERS = 'docker-containers';
public const string STEP_ACCOUNT_SETUP = 'account-setup';
+ public const string STEP_SSL_CERTIFICATE = 'ssl-certificate';
public const string STATUS_IN_PROGRESS = 'in-progress';
public const string STATUS_COMPLETED = 'completed';
@@ -135,6 +137,7 @@ class Server
// Register resources for dependency injection into actions
$config = $this->state->buildConfig();
+ $this->autoDetectUpgrade($config);
$paths = $this->paths;
$state = $this->state;
@@ -190,6 +193,77 @@ class Server
$adapter->start();
}
+ /**
+ * Auto-detect upgrade mode by checking for existing config files.
+ * Sets isUpgrade and lockedDatabase on the config when an existing
+ * installation is found and these values aren't already set.
+ */
+ private function autoDetectUpgrade(Config $config): void
+ {
+ if ($config->isUpgrade()) {
+ return;
+ }
+
+ $basePath = $config->isLocal() ? '/usr/src/code' : (getcwd() ?: '.');
+ $composePath = $basePath . '/docker-compose.yml';
+ $envPath = $basePath . '/.env';
+
+ if (!file_exists($composePath) && !file_exists($envPath)) {
+ return;
+ }
+
+ $config->setIsUpgrade(true);
+
+ if ($config->getLockedDatabase() !== null) {
+ return;
+ }
+
+ $database = $this->detectDatabaseFromFiles($composePath, $envPath);
+ if ($database !== null) {
+ $config->setLockedDatabase($database);
+ }
+ }
+
+ private function detectDatabaseFromFiles(string $composePath, string $envPath): ?string
+ {
+ $dbServices = ['mariadb', 'mongodb', 'postgresql'];
+
+ $composeData = @file_get_contents($composePath);
+ if ($composeData !== false) {
+ if (preg_match_all('/^\s*(?:container_name:\s*appwrite-(\w+)|(\w+):)\s*$/m', $composeData, $matches)) {
+ $serviceNames = array_filter(array_merge($matches[1], $matches[2]));
+ foreach ($dbServices as $db) {
+ if (in_array($db, $serviceNames, true)) {
+ return $db;
+ }
+ }
+ }
+ foreach ($dbServices as $db) {
+ if (preg_match('/^\s*' . preg_quote($db, '/') . ':\s*$/m', $composeData)) {
+ return $db;
+ }
+ }
+ }
+
+ $envData = @file_get_contents($envPath);
+ if ($envData !== false) {
+ if (preg_match('/^_APP_DB_ADAPTER=(.+)$/m', $envData, $m)) {
+ $adapter = trim($m[1], " \t\n\r\"'");
+ if (in_array($adapter, $dbServices, true)) {
+ return $adapter;
+ }
+ }
+ if (preg_match('/^_APP_DB_HOST=(.+)$/m', $envData, $m)) {
+ $host = trim($m[1], " \t\n\r\"'");
+ if (in_array($host, $dbServices, true)) {
+ return $host;
+ }
+ }
+ }
+
+ return null;
+ }
+
private function removeDockerInstallerContainer(string $container): void
{
$name = escapeshellarg($container);
diff --git a/src/Appwrite/Platform/Installer/Services/Http.php b/src/Appwrite/Platform/Installer/Services/Http.php
index bd0fc62cdc..b410e67a26 100644
--- a/src/Appwrite/Platform/Installer/Services/Http.php
+++ b/src/Appwrite/Platform/Installer/Services/Http.php
@@ -2,8 +2,10 @@
namespace Appwrite\Platform\Installer\Services;
+use Appwrite\Platform\Installer\Http\Installer\Certificate\Get as CertificateGet;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Install;
+use Appwrite\Platform\Installer\Http\Installer\Reset;
use Appwrite\Platform\Installer\Http\Installer\Shutdown;
use Appwrite\Platform\Installer\Http\Installer\Status;
use Appwrite\Platform\Installer\Http\Installer\Validate;
@@ -21,6 +23,8 @@ class Http extends Service
$this->addAction(Validate::getName(), new Validate());
$this->addAction(Complete::getName(), new Complete());
$this->addAction(Shutdown::getName(), new Shutdown());
+ $this->addAction(Reset::getName(), new Reset());
$this->addAction(Install::getName(), new Install());
+ $this->addAction(CertificateGet::getName(), new CertificateGet());
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Constants.php b/src/Appwrite/Platform/Modules/Databases/Constants.php
index cfc297c3f4..edc6b09cf0 100644
--- a/src/Appwrite/Platform/Modules/Databases/Constants.php
+++ b/src/Appwrite/Platform/Modules/Databases/Constants.php
@@ -22,3 +22,11 @@ const INDEX = 'index';
const DOCUMENTS = 'document';
const ATTRIBUTES = 'attribute';
const COLLECTIONS = 'collection';
+
+const LEGACY = 'legacy';
+const TABLESDB = 'tablesdb';
+const DOCUMENTSDB = 'documentsdb';
+const VECTORSDB = 'vectorsdb';
+
+const MIN_VECTOR_DIMENSION = 1;
+const MAX_VECTOR_DIMENSION = 16000;
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
index 728e732cc5..b2417871ed 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
@@ -10,7 +10,7 @@ use Utopia\Database\Operator;
class Action extends AppwriteAction
{
- private string $context = 'legacy';
+ private string $context = DATABASE_TYPE_LEGACY;
public function getDatabaseType(): string
{
@@ -20,7 +20,13 @@ class Action extends AppwriteAction
public function setHttpPath(string $path): AppwriteAction
{
if (\str_contains($path, '/tablesdb')) {
- $this->context = 'tablesdb';
+ $this->context = DATABASE_TYPE_TABLESDB;
+ }
+ if (\str_contains($path, '/documentsdb')) {
+ $this->context = DATABASE_TYPE_DOCUMENTSDB;
+ }
+ if (\str_contains($path, '/vectorsdb')) {
+ $this->context = DATABASE_TYPE_VECTORSDB;
}
return parent::setHttpPath($path);
}
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 f49d07ec4c..2f541936a8 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Action.php
@@ -15,6 +15,8 @@ abstract class Action extends UtopiaAction
*/
private ?string $context = COLLECTIONS;
+ private ?string $databaseType = LEGACY;
+
/**
* Get the response model used in the SDK and HTTP responses.
*/
@@ -24,6 +26,9 @@ abstract class Action extends UtopiaAction
{
if (\str_contains($path, '/tablesdb')) {
$this->context = TABLES;
+ $this->databaseType = TABLESDB;
+ } elseif (\str_contains($path, '/vectorsdb')) {
+ $this->databaseType = VECTORSDB;
}
return parent::setHttpPath($path);
}
@@ -36,6 +41,14 @@ abstract class Action extends UtopiaAction
return $this->context;
}
+ /**
+ * Get the current API database type.
+ */
+ protected function getDatabaseType(): string
+ {
+ return $this->databaseType;
+ }
+
/**
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
*/
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 216ec07e05..fd309a413c 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
@@ -84,12 +84,13 @@ class Create extends Action
->param('indexes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -121,12 +122,18 @@ class Create extends Action
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
}
+ /**
+ * @var Database $dbForDatabases
+ */
+ $dbForDatabases = $getDatabasesDB($database);
+
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
$databaseKey = 'database_' . $database->getSequence();
$attributesValidator = new AttributesValidator(
APP_LIMIT_ARRAY_PARAMS_SIZE,
- $dbForProject->getAdapter()->getSupportForSpatialAttributes()
+ $dbForDatabases->getAdapter()->getSupportForSpatialAttributes(),
+ $dbForDatabases->getAdapter()->getSupportForAttributes()
);
if (!$attributesValidator->isValid($attributes)) {
@@ -155,7 +162,7 @@ class Create extends Action
}
// Validate indexes
- $indexesValidator = new IndexesValidator($dbForProject->getLimitForIndexes());
+ $indexesValidator = new IndexesValidator($dbForDatabases->getLimitForIndexes());
if (!$indexesValidator->isValid($indexes)) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $indexesValidator->getDescription());
@@ -178,21 +185,23 @@ class Create extends Action
$indexValidator = new IndexValidator(
$collectionAttributes,
[],
- $dbForProject->getAdapter()->getMaxIndexLength(),
- $dbForProject->getAdapter()->getInternalIndexesKeys(),
- $dbForProject->getAdapter()->getSupportForIndexArray(),
- $dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
- $dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
- $dbForProject->getAdapter()->getSupportForVectors(),
- $dbForProject->getAdapter()->getSupportForAttributes(),
- $dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
- $dbForProject->getAdapter()->getSupportForIdenticalIndexes(),
- $dbForProject->getAdapter()->getSupportForObjectIndexes(),
- $dbForProject->getAdapter()->getSupportForTrigramIndex(),
- $dbForProject->getAdapter()->getSupportForSpatialAttributes(),
- $dbForProject->getAdapter()->getSupportForIndex(),
- $dbForProject->getAdapter()->getSupportForUniqueIndex(),
- $dbForProject->getAdapter()->getSupportForFulltextIndex(),
+ $dbForDatabases->getAdapter()->getMaxIndexLength(),
+ $dbForDatabases->getAdapter()->getInternalIndexesKeys(),
+ $dbForDatabases->getAdapter()->getSupportForIndexArray(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialIndexNull(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialIndexOrder(),
+ $dbForDatabases->getAdapter()->getSupportForVectors(),
+ $dbForDatabases->getAdapter()->getSupportForAttributes(),
+ $dbForDatabases->getAdapter()->getSupportForMultipleFulltextIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForIdenticalIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForObjectIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForTrigramIndex(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialAttributes(),
+ $dbForDatabases->getAdapter()->getSupportForIndex(),
+ $dbForDatabases->getAdapter()->getSupportForUniqueIndex(),
+ $dbForDatabases->getAdapter()->getSupportForFulltextIndex(),
+ $dbForDatabases->getAdapter()->getSupportForTTLIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForObject(),
);
foreach ($collectionIndexes as $indexDoc) {
@@ -203,7 +212,7 @@ class Create extends Action
}
try {
- $dbForProject->createCollection(
+ $dbForDatabases->createCollection(
id: $collectionKey,
attributes: $collectionAttributes,
indexes: $collectionIndexes,
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 7f194aa93d..7a5b73f7db 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php
@@ -62,13 +62,14 @@ class Delete extends Action
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -85,7 +86,8 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Failed to remove $type from DB");
}
- $dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
+ $dbForDatabases = $getDatabasesDB($database);
+ $dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_COLLECTION)
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 39146508fb..0bd4a2e080 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
@@ -17,6 +17,7 @@ abstract class Action extends DatabasesAction
* @var string|null The current context (either 'row' or 'document')
*/
private ?string $context = DOCUMENTS;
+ private ?string $databaseType = DATABASE_TYPE_LEGACY;
/**
* Get the response model used in the SDK and HTTP responses.
@@ -27,6 +28,10 @@ abstract class Action extends DatabasesAction
{
if (str_contains($path, '/tablesdb/')) {
$this->context = ROWS;
+ } elseif (str_contains($path, '/documentsdb/')) {
+ $this->databaseType = DATABASE_TYPE_DOCUMENTSDB;
+ } elseif (str_contains($path, '/vectorsdb/')) {
+ $this->databaseType = DATABASE_TYPE_VECTORSDB;
}
$contextId = '$' . $this->getCollectionsEventsContext() . 'Id';
@@ -45,6 +50,39 @@ abstract class Action extends DatabasesAction
return parent::setHttpPath($path);
}
+ protected function getDatabasesOperationReadMetric(): string
+ {
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return METRIC_DATABASES_OPERATIONS_READS;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASES_OPERATIONS_READS;
+ }
+
+ protected function getDatabasesIdOperationReadMetric(): string
+ {
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return METRIC_DATABASE_ID_OPERATIONS_READS;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_READS;
+ }
+
+ protected function getDatabasesOperationWriteMetric(): string
+ {
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return METRIC_DATABASES_OPERATIONS_WRITES;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASES_OPERATIONS_WRITES;
+
+ }
+
+ protected function getDatabasesIdOperationWriteMetric(): string
+ {
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return METRIC_DATABASE_ID_OPERATIONS_WRITES;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
+ }
+
/**
* Get the plural of the given name.
*
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 54557eaac0..8d31e19753 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
@@ -82,6 +82,7 @@ class Decrement extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('plan')
@@ -89,7 +90,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, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): 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, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -170,8 +171,9 @@ class Decrement extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
try {
- $document = $dbForProject->decreaseDocumentAttribute(
+ $document = $dbForDatabases->decreaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
@@ -201,8 +203,8 @@ class Decrement extends Action
);
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
+ ->addMetric($this->getDatabasesOperationWriteMetric(), 1)
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
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 b9c19b2d06..9de5f83154 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
@@ -82,6 +82,7 @@ class Increment extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('plan')
@@ -89,7 +90,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, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): 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, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -170,8 +171,9 @@ class Increment extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
try {
- $document = $dbForProject->increaseDocumentAttribute(
+ $document = $dbForDatabases->increaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
@@ -201,8 +203,8 @@ class Increment extends Action
);
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
+ ->addMetric($this->getDatabasesOperationWriteMetric(), 1)
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
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 f45b126f16..267a54adb0 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
@@ -76,6 +76,7 @@ class Delete extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
@@ -86,7 +87,7 @@ class Delete extends Action
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -163,10 +164,11 @@ class Delete extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
$documents = [];
try {
- $modified = $dbForProject->deleteDocuments(
+ $modified = $dbForDatabases->deleteDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
@@ -189,12 +191,12 @@ class Delete extends Action
}
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
+ ->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $modified))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $modified));
$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 000b59ff07..da3adf1192 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
@@ -80,6 +80,7 @@ class Update extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
@@ -90,7 +91,7 @@ class Update extends Action
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -189,11 +190,12 @@ class Update extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
$documents = [];
try {
- $modified = $dbForProject->withPreserveDates(function () use ($plan, &$documents, $dbForProject, $database, $collection, $data, $queries) {
- return $dbForProject->updateDocuments(
+ $modified = $dbForDatabases->withPreserveDates(function () use ($plan, &$documents, $dbForDatabases, $database, $collection, $data, $queries) {
+ return $dbForDatabases->updateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
new Document($data),
$queries,
@@ -220,8 +222,8 @@ class Update extends Action
}
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
+ ->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $modified))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
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 564b5ee7b6..050227b4b9 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
@@ -58,7 +58,7 @@ class Upsert extends Action
group: $this->getSDKGroup(),
name: self::getName(),
description: '/docs/references/databases/upsert-documents.md',
- auth: [AuthType::ADMIN, AuthType::KEY],
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
@@ -78,6 +78,7 @@ class Upsert extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
@@ -88,7 +89,7 @@ class Upsert extends Action
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -165,11 +166,12 @@ class Upsert extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
$upserted = [];
try {
- $modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) {
- return $dbForProject->upsertDocuments(
+ $modified = $dbForDatabases->withPreserveDates(function () use ($dbForDatabases, $database, $collection, $documents, $plan, &$upserted) {
+ return $dbForDatabases->upsertDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function (Document $document) use ($plan, &$upserted) {
@@ -195,8 +197,8 @@ class Upsert extends Action
}
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
+ ->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $modified))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
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 0bbe7c75cf..08c3b047be 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
@@ -85,7 +85,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),
+ new Parameter('transactionId', optional: true)
],
deprecated: new Deprecated(
since: '1.8.0',
@@ -110,7 +110,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),
+ new Parameter('transactionId', optional: true)
],
deprecated: new Deprecated(
since: '1.8.0',
@@ -127,6 +127,7 @@ class Create extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('user')
->inject('queueForEvents')
->inject('usage')
@@ -138,7 +139,7 @@ class Create extends Action
->inject('eventProcessor')
->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, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -447,11 +448,12 @@ class Create extends Action
return;
}
+ $dbForDatabases = $getDatabasesDB($database);
try {
$created = [];
- $dbForProject->withPreserveDates(
- function () use (&$created, $dbForProject, $database, $collection, $documents) {
- $dbForProject->createDocuments(
+ $dbForDatabases->withPreserveDates(
+ function () use (&$created, $dbForDatabases, $database, $collection, $documents) {
+ $dbForDatabases->createDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function ($doc) use (&$created) {
@@ -490,15 +492,15 @@ class Create extends Action
}
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
+ ->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $operations))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $operations)); // per collection
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
if ($isBulk) {
$response->dynamic(new Document([
'total' => count($created),
- $this->getSdkGroup() => $created
+ $this->getSDKGroup() => $created
]), $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 0996fa24ab..9931109c49 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
@@ -79,6 +79,7 @@ class Delete extends Action
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
@@ -95,6 +96,7 @@ class Delete extends Action
?\DateTime $requestTimestamp,
UtopiaResponse $response,
Database $dbForProject,
+ callable $getDatabasesDB,
Event $queueForEvents,
Context $usage,
TransactionState $transactionState,
@@ -116,14 +118,15 @@ class Delete extends Action
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
+ $dbForDatabases = $getDatabasesDB($database);
// Read permission should not be required for delete
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
if ($transactionId !== null) {
// Use transaction-aware document retrieval to see changes from same transaction
- $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
+ $document = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
} else {
- $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
+ $document = $authorization->skip(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId));
}
if ($document->isEmpty()) {
@@ -187,8 +190,8 @@ class Delete extends Action
}
try {
- $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
- $dbForProject->deleteDocument(
+ $dbForDatabases->withRequestTimestamp($requestTimestamp, function () use ($dbForDatabases, $database, $collection, $documentId) {
+ $dbForDatabases->deleteDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$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 10de481072..d84eb75a0f 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
@@ -68,13 +68,14 @@ class Get extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, TransactionState $transactionState, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -86,6 +87,7 @@ class Get extends Action
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
+ $dbForDatabases = $getDatabasesDB($database);
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
@@ -99,14 +101,17 @@ class Get extends Action
try {
$selects = Query::groupByType($queries)['selections'] ?? [];
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
+ $collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
// Use transaction-aware document retrieval if transactionId is provided
if ($transactionId !== null) {
- $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId, $queries);
- } elseif (!empty($selects)) {
- $document = $dbForProject->getDocument($collectionTableId, $documentId, $queries);
+ $document = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId, $queries);
+ } elseif (! empty($selects)) {
+ // has selects, allow relationship on documents!
+ $document = $dbForDatabases->getDocument($collectionTableId, $documentId, $queries);
} else {
- $document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument($collectionTableId, $documentId, $queries));
+ // has no selects, disable relationship looping on documents!
+ $document = $dbForDatabases->skipRelationships(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId, $queries));
}
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
@@ -129,8 +134,8 @@ class Get extends Action
);
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
+ ->addMetric($this->getDatabasesOperationReadMetric(), max($operations, 1))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), $operations);
$response->addHeader('X-Debug-Operations', $operations);
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 2e838329cb..4588e3666b 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
@@ -70,6 +70,7 @@ class XList extends Action
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
@@ -77,7 +78,7 @@ class XList extends Action
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Authorization $authorization, Audit $audit): void
+ public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Locale $locale, Reader $geodb, Authorization $authorization, Audit $audit): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -89,7 +90,8 @@ class XList extends Action
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
- $document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
+ $dbForDatabases = $getDatabasesDB($database);
+ $document = $dbForDatabases->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException(), params: [$documentId]);
}
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 ca7935dfbd..f006ad7f59 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,6 +83,7 @@ class Update extends Action
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
@@ -91,7 +92,7 @@ class Update extends Action
->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, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -118,15 +119,16 @@ class Update extends Action
$data = $this->parseOperators($data, $collection);
}
+ $dbForDatabases = $getDatabasesDB($database);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
if ($transactionId !== null) {
// Use transaction-aware document retrieval to see changes from same transaction
- $document = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
+ $document = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
} else {
- $document = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
+ $document = $authorization->skip(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId));
}
if ($document->isEmpty()) {
@@ -247,8 +249,8 @@ class Update extends Action
$setCollection($collection, $newDocument);
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
+ ->addMetric($this->getDatabasesOperationWriteMetric(), max($operations, 1))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), $operations);
// Handle transaction staging
if ($transactionId !== null) {
@@ -319,9 +321,9 @@ class Update extends Action
try {
- $document = $dbForProject->withRequestTimestamp(
+ $document = $dbForDatabases->withRequestTimestamp(
$requestTimestamp,
- fn () => $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument(
+ fn () => $dbForDatabases->withPreserveDates(fn () => $dbForDatabases->updateDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$document->getId(),
$newDocument
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 dc6655dfd3..0dfc64f392 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,6 +87,7 @@ class Upsert extends Action
->inject('response')
->inject('user')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
@@ -95,7 +96,7 @@ class Upsert extends Action
->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, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): 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, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -124,6 +125,7 @@ class Upsert extends Action
$data = $this->parseOperators($data, $collection);
}
+ $dbForDatabases = $getDatabasesDB($database);
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -134,13 +136,15 @@ class Upsert extends Action
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
+ $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)) {
if ($transactionId !== null) {
// Use transaction-aware document retrieval to see changes from same transaction
- $oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
+ $oldDocument = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
} else {
- $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
+ $oldDocument = $authorization->skip(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId));
}
if ($oldDocument->isEmpty()) {
if (!empty($user->getId())) {
@@ -182,7 +186,7 @@ class Upsert extends Action
$newDocument = new Document($data);
$operations = 0;
- $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations, $authorization) {
+ $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $dbForDatabases, $database, &$operations, $authorization) {
$operations++;
$relationships = \array_filter(
@@ -226,7 +230,7 @@ class Upsert extends Action
if ($relation instanceof Document) {
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
- $oldDocument = $authorization->skip(fn () => $dbForProject->getDocument(
+ $oldDocument = $authorization->skip(fn () => $dbForDatabases->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId()
));
@@ -257,8 +261,8 @@ class Upsert extends Action
$setCollection($collection, $newDocument);
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
+ ->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $operations))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $operations));
// Handle transaction staging
if ($transactionId !== null) {
@@ -327,8 +331,8 @@ class Upsert extends Action
$upserted = [];
try {
- $dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {
- return $dbForProject->upsertDocuments(
+ $dbForDatabases->withPreserveDates(function () use (&$upserted, $dbForDatabases, $database, $collection, $newDocument) {
+ return $dbForDatabases->upsertDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
[$newDocument],
onNext: function (Document $document) use (&$upserted) {
@@ -351,9 +355,9 @@ class Upsert extends Action
if (empty($upserted[0])) {
if ($transactionId !== null) {
// For transactions, get the document with transaction changes applied
- $upserted[0] = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
+ $upserted[0] = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
} else {
- $upserted[0] = $dbForProject->getDocument($collectionTableId, $documentId);
+ $upserted[0] = $dbForDatabases->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 a7d77d8a93..bc9d30c6f2 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
@@ -76,13 +76,14 @@ class XList extends Action
->inject('response')
->inject('dbForProject')
->inject('user')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Context $usage, TransactionState $transactionState, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -103,6 +104,7 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
+ $dbForDatabases = $getDatabasesDB($database);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
@@ -114,7 +116,7 @@ class XList extends Action
$documentId = $cursor->getValue();
- $cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
+ $cursorDocument = $authorization->skip(fn () => $dbForDatabases->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext());
@@ -127,11 +129,10 @@ class XList extends Action
try {
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
-
// Use transaction-aware document retrieval if transactionId is provided
if ($transactionId !== null) {
- $documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries);
- $total = $includeTotal ? $transactionState->countDocuments($collectionTableId, $transactionId, $queries) : 0;
+ $documents = $transactionState->listDocuments($database, $collectionTableId, $transactionId, $queries);
+ $total = $includeTotal ? $transactionState->countDocuments($database, $collectionTableId, $transactionId, $queries) : 0;
} elseif (! empty($selectQueries)) {
if ((int)$ttl > 0) {
@@ -170,7 +171,7 @@ class XList extends Action
}, $cachedDocuments);
$documentsCacheHit = true;
} else {
- $documents = $dbForProject->find($collectionTableId, $queries);
+ $documents = $dbForDatabases->find($collectionTableId, $queries);
// Convert Document objects to arrays for caching
$documentsArray = \array_map(function ($doc) {
@@ -196,15 +197,15 @@ class XList extends Action
} else {
// has selects, allow relationship on documents
- $documents = $dbForProject->find($collectionTableId, $queries);
- $total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
+ $documents = $dbForDatabases->find($collectionTableId, $queries);
+ $total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
}
} else {
// has no selects, disable relationship loading on documents
/* @type Document[] $documents */
- $documents = $dbForProject->skipRelationships(fn () => $dbForProject->find($collectionTableId, $queries));
- $total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
+ $documents = $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
+ $total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
}
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
@@ -232,8 +233,8 @@ class XList extends Action
}
$usage
- ->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
- ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
+ ->addMetric($this->getDatabasesOperationReadMetric(), max($operations, 1))
+ ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationReadMetric()), $operations);
$response->dynamic(new Document([
'total' => $total,
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 fd785f3609..7e073c95d4 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
@@ -77,13 +77,14 @@ class Create extends Action
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -103,7 +104,9 @@ class Create extends Action
Query::equal('databaseInternalId', [$db->getSequence()])
], 61);
- $limit = $dbForProject->getLimitForIndexes();
+ $dbForDatabases = $getDatabasesDB($db);
+
+ $limit = $dbForDatabases->getLimitForIndexes();
if ($count >= $limit) {
throw new Exception($this->getLimitException(), params: [$collectionId]);
@@ -145,32 +148,35 @@ class Create extends Action
];
$contextType = $this->getParentContext();
- foreach ($attributes as $i => $attribute) {
- $attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
+ if ($dbForDatabases->getAdapter()->getSupportForAttributes()) {
+ foreach ($attributes as $i => $attribute) {
+ // find attribute metadata in collection document
+ $attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
- if ($attributeIndex === false) {
- throw new Exception($this->getParentUnknownException(), params: [$attribute]);
- }
+ if ($attributeIndex === false) {
+ throw new Exception($this->getParentUnknownException(), params: [$attribute]);
+ }
- $attributeStatus = $oldAttributes[$attributeIndex]['status'];
- $attributeType = $oldAttributes[$attributeIndex]['type'];
- $attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
+ $attributeStatus = $oldAttributes[$attributeIndex]['status'];
+ $attributeType = $oldAttributes[$attributeIndex]['type'];
+ $attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
- if ($attributeType === Database::VAR_RELATIONSHIP) {
- throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship $contextType: " . $oldAttributes[$attributeIndex]['key']);
- }
+ if ($attributeType === Database::VAR_RELATIONSHIP) {
+ throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship $contextType: " . $oldAttributes[$attributeIndex]['key']);
+ }
- if ($attributeStatus !== 'available') {
- throw new Exception($this->getParentNotAvailableException(), params: [$oldAttributes[$attributeIndex]['key']]);
- }
+ if ($attributeStatus !== 'available') {
+ throw new Exception($this->getParentNotAvailableException(), params: [$oldAttributes[$attributeIndex]['key']]);
+ }
- if (empty($lengths[$i])) {
- $lengths[$i] = null;
- }
+ if (empty($lengths[$i])) {
+ $lengths[$i] = null;
+ }
- if ($attributeArray === true) {
- // Because of a bug in MySQL, we cannot create indexes on array attributes for now, otherwise queries break.
- throw new Exception(Exception::INDEX_INVALID, 'Creating indexes on array attributes is not currently supported.');
+ if ($attributeArray === true) {
+ // Because of a bug in MySQL, we cannot create indexes on array attributes for now, otherwise queries break.
+ throw new Exception(Exception::INDEX_INVALID, 'Creating indexes on array attributes is not currently supported.');
+ }
}
}
@@ -191,21 +197,23 @@ class Create extends Action
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$collection->getAttribute('indexes'),
- $dbForProject->getAdapter()->getMaxIndexLength(),
- $dbForProject->getAdapter()->getInternalIndexesKeys(),
- $dbForProject->getAdapter()->getSupportForIndexArray(),
- $dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
- $dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
- $dbForProject->getAdapter()->getSupportForVectors(),
- $dbForProject->getAdapter()->getSupportForAttributes(),
- $dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
- $dbForProject->getAdapter()->getSupportForIdenticalIndexes(),
- $dbForProject->getAdapter()->getSupportForObjectIndexes(),
- $dbForProject->getAdapter()->getSupportForTrigramIndex(),
- $dbForProject->getAdapter()->getSupportForSpatialAttributes(),
- $dbForProject->getAdapter()->getSupportForIndex(),
- $dbForProject->getAdapter()->getSupportForUniqueIndex(),
- $dbForProject->getAdapter()->getSupportForFulltextIndex(),
+ $dbForDatabases->getAdapter()->getMaxIndexLength(),
+ $dbForDatabases->getAdapter()->getInternalIndexesKeys(),
+ $dbForDatabases->getAdapter()->getSupportForIndexArray(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialIndexNull(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialIndexOrder(),
+ $dbForDatabases->getAdapter()->getSupportForVectors(),
+ $dbForDatabases->getAdapter()->getSupportForAttributes(),
+ $dbForDatabases->getAdapter()->getSupportForMultipleFulltextIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForIdenticalIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForObjectIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForTrigramIndex(),
+ $dbForDatabases->getAdapter()->getSupportForSpatialAttributes(),
+ $dbForDatabases->getAdapter()->getSupportForIndex(),
+ $dbForDatabases->getAdapter()->getSupportForUniqueIndex(),
+ $dbForDatabases->getAdapter()->getSupportForFulltextIndex(),
+ $dbForDatabases->getAdapter()->getSupportForTTLIndexes(),
+ $dbForDatabases->getAdapter()->getSupportForObject()
);
if (!$validator->isValid($index)) {
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 f34fd82997..5d9d425d71 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
@@ -70,12 +70,13 @@ class Update extends Action
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -110,7 +111,8 @@ class Update extends Action
->setAttribute('search', \implode(' ', [$collectionId, $searchName]))
);
- $dbForProject->updateCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $permissions, $documentSecurity);
+ $dbForDatabases = $getDatabasesDB($database);
+ $dbForDatabases->updateCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $permissions, $documentSecurity);
$queueForEvents
->setContext('database', $database)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php
index de20d058c4..37213f1061 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Usage/Get.php
@@ -31,6 +31,11 @@ class Get extends Action
return UtopiaResponse::MODEL_USAGE_COLLECTION;
}
+ protected function getMetric(): string
+ {
+ return METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
+ }
+
public function __construct()
{
$this
@@ -64,14 +69,16 @@ class Get extends Action
->inject('response')
->inject('dbForProject')
->inject('authorization')
+ ->inject('getDatabasesDB')
->callback($this->action(...));
}
- public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void
+ public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject, Authorization $authorization, callable $getDatabasesDB): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
- $collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
+ $dbForDatabases = $getDatabasesDB($database);
+ $collection = $dbForDatabases->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
@@ -81,7 +88,7 @@ class Get extends Action
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
- str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collectionDocument->getSequence()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS),
+ str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collectionDocument->getSequence()], $this->getMetric()),
];
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php
index c2786b9f26..3585bc4477 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php
@@ -19,7 +19,9 @@ use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
+use Utopia\DSN\DSN;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
+use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
@@ -30,6 +32,121 @@ class Create extends Action
return 'createDatabase';
}
+ protected function getDatabaseDSN(Document $project): string
+ {
+ // TODO: use database worker for for creating the v2 schema if not present
+ // it is considered that the v2 metadata schema is already created during server start in the http.php
+ return $this->constructDatabaseDSNFromProjectDatabase($this->getDatabaseType(), $project->getAttribute('region'), $project->getAttribute('database'));
+ }
+
+ private function constructDatabaseDSNFromProjectDatabase(string $databasetype, $region, ?string $dsn = null): string
+ {
+ $databases = [];
+ $databaseKeys = [];
+ /**
+ * @var string|null $databaseOverride
+ */
+ $databaseOverride = '';
+ $dbScheme = '';
+ $databaseSharedTables = [];
+ $databaseSharedTablesV1 = [];
+ $databaseSharedTablesV2 = [];
+ $projectSharedTables = [];
+ $projectSharedTablesV1 = [];
+ $projectSharedTablesV2 = [];
+
+ switch ($databasetype) {
+ case DOCUMENTSDB:
+ $databases = Config::getParam('pools-documentsdb', []);
+ $databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
+ $databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
+ $dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
+ $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
+ $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
+ break;
+ case VECTORSDB:
+ $databases = Config::getParam('pools-vectorsdb', []);
+ $databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
+ $databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
+ $dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
+ $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
+ $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
+ break;
+ default:
+ // legacy/tablesdb
+ // it is already created during create project
+ return $dsn;
+ }
+
+ $isSharedTablesV1 = false;
+ $isSharedTablesV2 = false;
+
+ if (!empty($dsn)) {
+ try {
+ $parsedDsn = new DSN($dsn);
+ $dsnHost = $parsedDsn->getHost();
+ } catch (\InvalidArgumentException) {
+ $dsnHost = $dsn;
+ }
+
+ $projectSharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
+ $projectSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
+ $projectSharedTablesV2 = \array_diff($projectSharedTables, $projectSharedTablesV1);
+ $isSharedTablesV1 = \in_array($dsnHost, $projectSharedTablesV1);
+ $isSharedTablesV2 = \in_array($dsnHost, $projectSharedTablesV2);
+ }
+
+ if ($region !== 'default') {
+ $keys = explode(',', $databaseKeys);
+ $databases = array_filter($keys, function ($value) use ($region) {
+ return str_contains($value, $region);
+ });
+ }
+ $databaseSharedTablesV2 = \array_diff($databaseSharedTables, $databaseSharedTablesV1);
+
+ $index = \array_search($databaseOverride, $databases);
+ if ($index !== false) {
+ $selectedDsn = $databases[$index];
+ } else {
+ if (!empty($dsn)) {
+ $beforeFilter = \array_values($databases);
+ if ($isSharedTablesV1) {
+ $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
+ } elseif ($isSharedTablesV2) {
+ $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV2));
+ } else {
+ $databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
+ }
+ }
+ $selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
+ }
+
+ if (\in_array($selectedDsn, $databaseSharedTables)) {
+ $schema = 'appwrite';
+ $database = 'appwrite';
+ $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', '');
+ $selectedDsn = $schema . '://' . $selectedDsn . '?database=' . $database;
+
+ if (!empty($namespace)) {
+ $selectedDsn .= '&namespace=' . $namespace;
+ }
+ }
+ try {
+ new DSN($selectedDsn);
+ } catch (\InvalidArgumentException) {
+ $selectedDsn = $dbScheme.'://' . $selectedDsn;
+ }
+
+ return $selectedDsn;
+ }
+
+ protected function getDatabaseCollection()
+ {
+ return match ($this->getDatabaseType()) {
+ 'vectorsdb' => (Config::getParam('collections', [])['vectorsdb'] ?? [])['collections'] ?? [],
+ default => (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? [],
+ };
+ }
public function __construct()
{
$this
@@ -65,13 +182,15 @@ class Create extends Action
->param('databaseId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique 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.', false, ['dbForProject'])
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('project')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->callback($this->action(...));
}
- public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
+ public function action(string $databaseId, string $name, bool $enabled, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents): void
{
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
@@ -82,6 +201,7 @@ class Create extends Action
'enabled' => $enabled,
'search' => implode(' ', [$databaseId, $name]),
'type' => $this->getDatabaseType(),
+ 'database' => $this->getDatabaseDSN($project)
]));
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS, params: [$databaseId]);
@@ -91,7 +211,7 @@ class Create extends Action
$database = $dbForProject->getDocument('databases', $databaseId);
- $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? [];
+ $collections = $this->getDatabaseCollection();
if (empty($collections)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.');
}
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 e2a4491736..f3edf010d4 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
@@ -10,11 +10,45 @@ abstract class Action extends DatabasesAction
* The current API context (either 'table' or 'collection').
*/
private ?string $context = COLLECTIONS;
+ private ?string $databaseType = LEGACY;
+
+ public function getDatabaseType(): string
+ {
+ return $this->databaseType;
+ }
+
+ protected function getDatabasesOperationWriteMetric(): string
+ {
+ if ($this->databaseType === LEGACY || $this->databaseType === TABLESDB) {
+ return METRIC_DATABASES_OPERATIONS_WRITES;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASES_OPERATIONS_WRITES;
+
+ }
+ protected function getDatabasesIdOperationWriteMetric(): string
+ {
+ if ($this->databaseType === LEGACY || $this->databaseType === TABLESDB) {
+ return METRIC_DATABASE_ID_OPERATIONS_WRITES;
+ }
+ return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
+ }
public function setHttpPath(string $path): DatabasesAction
{
- if (\str_contains($path, '/tablesdb')) {
- $this->context = TABLES;
+ switch (true) {
+ case str_contains($path, '/tablesdb'):
+ $this->context = TABLES;
+ $this->databaseType = TABLESDB;
+ break;
+
+ case str_contains($path, '/documentsdb'):
+ $this->context = COLLECTIONS;
+ $this->databaseType = DOCUMENTSDB;
+ break;
+ case str_contains($path, '/vectorsdb'):
+ $this->context = COLLECTIONS;
+ $this->databaseType = VECTORSDB;
+ break;
}
return parent::setHttpPath($path);
}
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 eebb3a77d5..26457cc4a0 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
@@ -148,7 +148,7 @@ class Create extends Action
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
$isDependant = isset($dependants[$collectionKey][$documentId]);
- $document = $transactionState->getDocument($collectionKey, $documentId, $transactionId);
+ $document = $transactionState->getDocument($database, $collectionKey, $documentId, $transactionId);
if ($document->isEmpty() && !$isDependant && $operation['action'] !== 'upsert') {
throw new Exception(Exception::DOCUMENT_NOT_FOUND, params: [$documentId]);
}
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 9a5a63ea91..5e88eee500 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
@@ -67,8 +67,10 @@ class Update extends Action
->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject'])
->param('commit', false, new Boolean(), 'Commit transaction?', true)
->param('rollback', false, new Boolean(), 'Rollback transaction?', true)
+ ->inject('project')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
->inject('queueForDeletes')
@@ -88,6 +90,7 @@ class Update extends Action
* @param bool $rollback
* @param UtopiaResponse $response
* @param Database $dbForProject
+ * @param callable $getDatabasesDB
* @param Document $user
* @param TransactionState $transactionState
* @param Delete $queueForDeletes
@@ -106,7 +109,7 @@ class Update extends Action
* @throws Structure
* @throws \Utopia\Http\Exception
*/
- public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
+ public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
{
if (!$commit && !$rollback) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
@@ -135,14 +138,52 @@ class Update extends Action
}
if ($commit) {
-
$operations = [];
$totalOperations = 0;
$databaseOperations = [];
$currentDocumentId = null;
+ $firstOperation = $authorization->skip(fn () => $dbForProject->findOne('transactionLogs', [
+ Query::equal('transactionInternalId', [$transaction->getSequence()]),
+ Query::orderAsc(),
+ ]));
+
+ if ($firstOperation->isEmpty()) {
+ $transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
+ 'transactions',
+ $transactionId,
+ new Document(['status' => 'committed'])
+ ));
+
+ $queueForDeletes
+ ->setType(DELETE_TYPE_DOCUMENT)
+ ->setDocument($transaction);
+
+ $response
+ ->setStatusCode(SwooleResponse::STATUS_CODE_OK)
+ ->dynamic($transaction, $this->getResponseModel());
+
+ return;
+ }
+
+ $databaseDoc = null;
+ switch ($this->getDatabaseType()) {
+ case DATABASE_TYPE_DOCUMENTSDB:
+ case DATABASE_TYPE_VECTORSDB:
+ $databaseDoc = $authorization->skip(fn () => $dbForProject->findOne('databases', [
+ Query::equal('$sequence', [$firstOperation['databaseInternalId']])
+ ]));
+ break;
+ default:
+ // Legacy/tablesdb: use project-level database
+ $databaseDoc = new Document(['database' => $project->getAttribute('database')]);
+ break;
+ }
+
+ $dbForDatabases = $getDatabasesDB($databaseDoc);
+
try {
- $dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
+ $dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
@@ -182,7 +223,7 @@ class Update extends Action
}
if ($action === 'delete' && $documentId && empty($data)) {
- $doc = $dbForProject->getDocument($collectionId, $documentId);
+ $doc = $dbForDatabases->getDocument($collectionId, $documentId);
if (!$doc->isEmpty()) {
$operation['data'] = $doc->getArrayCopy();
$data = $operation['data'];
@@ -196,40 +237,40 @@ class Update extends Action
switch ($action) {
case 'create':
- $this->handleCreateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
+ $this->handleCreateOperation($dbForDatabases, $collectionId, $documentId, $data, $createdAt, $state);
break;
case 'update':
- $this->handleUpdateOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
+ $this->handleUpdateOperation($dbForDatabases, $collectionId, $documentId, $data, $createdAt, $state);
break;
case 'upsert':
- $this->handleUpsertOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
+ $this->handleUpsertOperation($dbForDatabases, $collectionId, $documentId, $data, $createdAt, $state);
break;
case 'delete':
- $this->handleDeleteOperation($dbForProject, $collectionId, $documentId, $createdAt, $state);
+ $this->handleDeleteOperation($dbForDatabases, $collectionId, $documentId, $createdAt, $state);
break;
case 'increment':
- $this->handleIncrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
+ $this->handleIncrementOperation($dbForDatabases, $collectionId, $documentId, $data, $createdAt, $state);
break;
case 'decrement':
- $this->handleDecrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
+ $this->handleDecrementOperation($dbForDatabases, $collectionId, $documentId, $data, $createdAt, $state);
break;
case 'bulkCreate':
- $count = $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
+ $count = $this->handleBulkCreateOperation($dbForDatabases, $collectionId, $data, $createdAt, $state);
$totalOperations += $count;
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
break;
case 'bulkUpdate':
- $count = $this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
+ $count = $this->handleBulkUpdateOperation($dbForDatabases, $transactionState, $collectionId, $data, $createdAt, $state);
$totalOperations += $count;
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
break;
case 'bulkUpsert':
- $count = $this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
+ $count = $this->handleBulkUpsertOperation($dbForDatabases, $transactionState, $collectionId, $data, $createdAt, $state);
$totalOperations += $count;
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
break;
case 'bulkDelete':
- $count = $this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
+ $count = $this->handleBulkDeleteOperation($dbForDatabases, $transactionState, $collectionId, $data, $createdAt, $state);
$totalOperations += $count;
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
break;
@@ -279,15 +320,16 @@ class Update extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
- $usage->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
+ $usage->addMetric($this->getDatabasesOperationWriteMetric(), $totalOperations);
foreach ($databaseOperations as $sequence => $count) {
$usage->addMetric(
- str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES),
+ str_replace('{databaseInternalId}', $sequence, $this->getDatabasesIdOperationWriteMetric()),
$count
);
}
+ $dbCache = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
$collectionInternalId = $operation['collectionInternalId'];
@@ -300,6 +342,16 @@ class Update extends Action
$data = $data->getArrayCopy();
}
+ // using a dbCache so only one time database is set with databaseInternalId
+ if (!isset($dbCache[$databaseInternalId])) {
+ $databaseDoc = $authorization->skip(fn () => $dbForProject->findOne('databases', [
+ Query::equal('$sequence', [$databaseInternalId])
+ ]));
+ $dbCache[$databaseInternalId] = $getDatabasesDB($databaseDoc);
+ }
+
+ $dbForDatabases = $dbCache[$databaseInternalId];
+
$database = $authorization->skip(fn () => $dbForProject->findOne('databases', [
Query::equal('$sequence', [$databaseInternalId])
]));
@@ -329,7 +381,7 @@ class Update extends Action
$eventAction = 'create';
$docId = $documentId ?? $data['$id'] ?? null;
if ($docId) {
- $doc = $dbForProject->getDocument($collectionId, $docId);
+ $doc = $dbForDatabases->getDocument($collectionId, $docId);
if (!$doc->isEmpty()) {
$documentsToTrigger[] = $doc;
}
@@ -340,7 +392,7 @@ class Update extends Action
case 'decrement':
$eventAction = 'update';
if ($documentId) {
- $doc = $dbForProject->getDocument($collectionId, $documentId);
+ $doc = $dbForDatabases->getDocument($collectionId, $documentId);
if (!$doc->isEmpty()) {
$documentsToTrigger[] = $doc;
}
@@ -356,7 +408,7 @@ class Update extends Action
$eventAction = 'update';
$docId = $documentId ?? $data['$id'] ?? null;
if ($docId) {
- $doc = $dbForProject->getDocument($collectionId, $docId);
+ $doc = $dbForDatabases->getDocument($collectionId, $docId);
if (!$doc->isEmpty()) {
$documentsToTrigger[] = $doc;
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php
index 6f90e77e2b..18e6fd7a8b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/Get.php
@@ -26,6 +26,43 @@ class Get extends Action
return 'getDatabaseUsage';
}
+ protected $databaseType = DATABASE_TYPE_LEGACY;
+
+ public function setHttpPath(string $path): Action
+ {
+ $this->databaseType = match (true) {
+ str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+ str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
+ default => DATABASE_TYPE_LEGACY,
+ };
+
+ return parent::setHttpPath($path);
+ }
+
+ protected function getMetrics(): array
+ {
+ $metrics = [
+ METRIC_DATABASE_ID_COLLECTIONS,
+ METRIC_DATABASE_ID_DOCUMENTS,
+ METRIC_DATABASE_ID_STORAGE,
+ METRIC_DATABASE_ID_OPERATIONS_READS,
+ METRIC_DATABASE_ID_OPERATIONS_WRITES
+ ];
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return $metrics;
+ }
+
+ return array_map(
+ fn ($metric) => "{$this->databaseType}.{$metric}",
+ $metrics
+ );
+ }
+
+ protected function getResponseModel(): string
+ {
+ return UtopiaResponse::MODEL_USAGE_DATABASE;
+ }
+
public function __construct()
{
$this
@@ -74,13 +111,10 @@ class Get extends Action
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
- $metrics = [
- str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_COLLECTIONS),
- str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_DOCUMENTS),
- str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_STORAGE),
- str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS),
- str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES)
- ];
+ $metrics = array_map(
+ fn ($metric) => str_replace('{databaseInternalId}', $database->getSequence(), $metric),
+ $this->getMetrics()
+ );
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
@@ -142,6 +176,6 @@ class Get extends Action
'storage' => $usage[$metrics[2]]['data'],
'databaseReads' => $usage[$metrics[3]]['data'],
'databaseWrites' => $usage[$metrics[4]]['data'],
- ]), UtopiaResponse::MODEL_USAGE_DATABASE);
+ ]), $this->getResponseModel());
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php
index db5ad21358..b8cb774a3e 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Usage/XList.php
@@ -24,6 +24,43 @@ class XList extends Action
return 'listDatabaseUsage';
}
+ protected $databaseType = DATABASE_TYPE_LEGACY;
+
+ public function setHttpPath(string $path): Action
+ {
+ $this->databaseType = match (true) {
+ str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+ str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
+ default => DATABASE_TYPE_LEGACY,
+ };
+
+ return parent::setHttpPath($path);
+ }
+
+ protected function getMetrics(): array
+ {
+ $metrics = [
+ METRIC_DATABASES,
+ METRIC_COLLECTIONS,
+ METRIC_DOCUMENTS,
+ METRIC_DATABASES_STORAGE,
+ METRIC_DATABASES_OPERATIONS_READS,
+ METRIC_DATABASES_OPERATIONS_WRITES,
+ ];
+ if ($this->databaseType === DATABASE_TYPE_LEGACY || $this->databaseType === DATABASE_TYPE_TABLESDB) {
+ return $metrics;
+ }
+ return array_map(
+ fn ($metric) => "{$this->databaseType}.{$metric}",
+ $metrics
+ );
+ }
+
+ protected function getResponseModel(): string
+ {
+ return UtopiaResponse::MODEL_USAGE_DATABASES;
+ }
+
public function __construct()
{
$this
@@ -66,14 +103,7 @@ class XList extends Action
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
- $metrics = [
- METRIC_DATABASES,
- METRIC_COLLECTIONS,
- METRIC_DOCUMENTS,
- METRIC_DATABASES_STORAGE,
- METRIC_DATABASES_OPERATIONS_READS,
- METRIC_DATABASES_OPERATIONS_WRITES,
- ];
+ $metrics = $this->getMetrics();
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
@@ -136,6 +166,6 @@ class XList extends Action
'storage' => $usage[$metrics[3]]['data'],
'databasesReads' => $usage[$metrics[4]]['data'],
'databasesWrites' => $usage[$metrics[5]]['data'],
- ]), UtopiaResponse::MODEL_USAGE_DATABASES);
+ ]), $this->getResponseModel());
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php
index 8627fa49c5..21dbc83edc 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php
@@ -17,7 +17,6 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
-use Utopia\Platform\Action;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
@@ -28,6 +27,11 @@ class XList extends Action
return 'listDatabases';
}
+ protected function getDatabaseTypeQueryFilters(): array
+ {
+ return [Query::equal('type', [$this->getDatabaseType()])];
+ }
+
public function __construct()
{
$this
@@ -93,6 +97,7 @@ class XList extends Action
}
try {
+ $queries = array_merge($queries, $this->getDatabaseTypeQueryFilters());
$databases = $dbForProject->find('databases', $queries);
$total = $includeTotal ? $dbForProject->count('databases', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Create.php
new file mode 100644
index 0000000000..d1e91addf7
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Create.php
@@ -0,0 +1,75 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections')
+ ->desc('Create collection')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].create')
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'collections.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'collections',
+ name: 'createCollection',
+ description: '/docs/references/documentsdb/create-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique 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.', false, ['dbForProject'])
+ ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
+ ->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
+ ->param('attributes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
+ ->param('indexes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php
new file mode 100644
index 0000000000..d698b40203
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php
@@ -0,0 +1,62 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId')
+ ->desc('Delete collection')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].delete')
+ ->label('audits.event', 'collection.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'collections',
+ name: 'deleteCollection',
+ description: '/docs/references/documentsdb/delete-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Decrement.php
new file mode 100644
index 0000000000..de3acdc96a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Decrement.php
@@ -0,0 +1,73 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/:attribute/decrement')
+ ->desc('Decrement document attribute')
+ ->groups(['api', 'database'])
+ ->label('event', 'documentsdb.[databaseId].collections.[collectionId].documents.[documentId].update')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'decrementDocumentAttribute',
+ description: '/docs/references/documentsdb/decrement-document-attribute.md',
+ auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->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 the operation.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Increment.php
new file mode 100644
index 0000000000..8664bb09ec
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Attribute/Increment.php
@@ -0,0 +1,73 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/:attribute/increment')
+ ->desc('Increment document attribute')
+ ->groups(['api', 'database'])
+ ->label('event', 'documentsdb.[databaseId].collections.[collectionId].documents.[documentId].update')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'incrementDocumentAttribute',
+ description: '/docs/references/documentsdb/increment-document-attribute.md',
+ auth: [AuthType::SESSION, AuthType::JWT, AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->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('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
new file mode 100644
index 0000000000..09ad9a5741
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
@@ -0,0 +1,72 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Delete documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteDocuments',
+ description: '/docs/references/documentsdb/delete-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
new file mode 100644
index 0000000000..c723f1bc30
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
@@ -0,0 +1,74 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Update documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'updateDocuments',
+ description: '/docs/references/documentsdb/update-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
new file mode 100644
index 0000000000..d5b62ec903
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
@@ -0,0 +1,74 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Upsert documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'upsertDocuments',
+ description: '/docs/references/documentsdb/upsert-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ )
+ ])
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
new file mode 100644
index 0000000000..039a05ff50
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
@@ -0,0 +1,116 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Create document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'createDocument',
+ desc: 'Create document',
+ description: '/docs/references/documentsdb/create-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ parameters: [
+ new Parameter('databaseId', optional: false),
+ new Parameter('collectionId', optional: false),
+ new Parameter('documentId', optional: false),
+ new Parameter('data', optional: false),
+ new Parameter('permissions', optional: true),
+ ]
+ ),
+ new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'createDocuments',
+ desc: 'Create documents',
+ description: '/docs/references/documentsdb/create-documents.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getBulkResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ parameters: [
+ new Parameter('databaseId', optional: false),
+ new Parameter('collectionId', optional: false),
+ new Parameter('documents', optional: false),
+ ]
+ )
+ ])
+ ->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('data', [], new JSON(), 'Document data as JSON object.', true, example: '{"username":"walter.obrien","email":"walter.obrien@example.com","fullName":"Walter O\'Brien","age":30,"isAdmin":false}')
+ ->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('getDatabasesDB')
+ ->inject('user')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('authorization')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Delete.php
new file mode 100644
index 0000000000..86749f8a3d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Delete.php
@@ -0,0 +1,76 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Delete document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
+ ->label('audits.event', 'document.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteDocument',
+ description: '/docs/references/documentsdb/delete-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->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')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Get.php
new file mode 100644
index 0000000000..4dd1f6f6b3
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Get.php
@@ -0,0 +1,64 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Get document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'getDocument',
+ description: '/docs/references/documentsdb/get-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php
new file mode 100644
index 0000000000..cc7fe41555
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php
@@ -0,0 +1,59 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
+ ->desc('List document logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'logs',
+ name: 'listDocumentLogs',
+ description: '/docs/references/documentsdb/get-document-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Update.php
new file mode 100644
index 0000000000..b5c612c155
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Update.php
@@ -0,0 +1,75 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Update document')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'updateDocument',
+ description: '/docs/references/documentsdb/update-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->param('data', [], new JSON(), 'Document data as JSON object. Include only fields 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')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Upsert.php
new file mode 100644
index 0000000000..448c2d44bc
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Upsert.php
@@ -0,0 +1,78 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Upsert a document')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.upsert')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'upsertDocument',
+ description: '/docs/references/documentsdb/upsert-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->param('data', [], new JSON(), 'Document data as JSON object. Include all required fields of the document 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')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/XList.php
new file mode 100644
index 0000000000..9e0d0b10d9
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/XList.php
@@ -0,0 +1,68 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('List documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'listDocuments',
+ description: '/docs/references/documentsdb/list-documents.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('user')
+ ->inject('getDatabasesDB')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Get.php
new file mode 100644
index 0000000000..53120dd636
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Get.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId')
+ ->desc('Get collection')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'collections',
+ name: 'getCollection',
+ description: '/docs/references/documentsdb/get-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php
new file mode 100644
index 0000000000..3aee3ebcb1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php
@@ -0,0 +1,73 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/indexes')
+ ->desc('Create index')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create')
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'index.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'createIndex',
+ description: '/docs/references/documentsdb/create-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_ACCEPTED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject'])
+ ->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
+ ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.')
+ ->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject'])
+ ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
+ ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php
new file mode 100644
index 0000000000..d4464f171d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php
@@ -0,0 +1,67 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/indexes/:key')
+ ->desc('Delete index')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update')
+ ->label('audits.event', 'index.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteIndex', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/documentsdb/delete-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->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('key', '', new Key(), 'Index Key.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Get.php
new file mode 100644
index 0000000000..7fa75b6ed9
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Get.php
@@ -0,0 +1,58 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/indexes/:key')
+ ->desc('Get index')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'getIndex', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/documentsdb/get-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('key', null, new Key(), 'Index Key.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/XList.php
new file mode 100644
index 0000000000..1e16155f76
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/XList.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/indexes')
+ ->desc('List indexes')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'listIndexes', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/documentsdb/list-indexes.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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 Indexes(), '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. You may filter on the following attributes: ' . implode(', ', Indexes::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php
new file mode 100644
index 0000000000..51695ea165
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php
@@ -0,0 +1,58 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
+ ->desc('List collection logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: $this->getSdkGroup(),
+ name: 'listCollectionLogs',
+ description: '/docs/references/documentsdb/get-collection-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Update.php
new file mode 100644
index 0000000000..052970fec4
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Update.php
@@ -0,0 +1,68 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId')
+ ->desc('Update collection')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].update')
+ ->label('audits.event', 'collection.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'collections',
+ name: 'updateCollection',
+ description: '/docs/references/documentsdb/update-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_COLLECTION,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
+ ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php
new file mode 100644
index 0000000000..51dd3c381d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Usage/Get.php
@@ -0,0 +1,65 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/usage')
+ ->desc('Get collection usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: null,
+ name: 'getCollectionUsage',
+ description: '/docs/references/documentsdb/get-collection-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->inject('getDatabasesDB')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/XList.php
new file mode 100644
index 0000000000..638244145b
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/XList.php
@@ -0,0 +1,61 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/collections')
+ ->desc('List collections')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'collections',
+ name: 'listCollections',
+ description: '/docs/references/documentsdb/list-collections.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('queries', [], new Collections(), '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. You may filter on the following attributes: ' . implode(', ', Collections::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Create.php
new file mode 100644
index 0000000000..f9b425b3e6
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Create.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb')
+ ->desc('Create database')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].create')
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'database.create')
+ ->label('audits.resource', 'database/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'documentsdb',
+ name: 'create',
+ description: '/docs/references/documentsdb/create.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique 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.', false, ['dbForProject'])
+ ->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
+ ->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('project')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php
new file mode 100644
index 0000000000..1708656c98
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/:databaseId')
+ ->desc('Delete database')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].delete')
+ ->label('audits.event', 'database.delete')
+ ->label('audits.resource', 'database/{request.databaseId}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'documentsdb',
+ name: 'delete',
+ description: '/docs/references/documentsdb/delete.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Get.php
new file mode 100644
index 0000000000..309a3b867e
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Get.php
@@ -0,0 +1,50 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId')
+ ->desc('Get database')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'documentsdb',
+ name: 'get',
+ description: '/docs/references/documentsdb/get.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Logs/XList.php
new file mode 100644
index 0000000000..8afb0fd1ef
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Logs/XList.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/logs')
+ ->desc('List database logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: 'logs',
+ name: 'listDatabaseLogs',
+ description: '/docs/references/documentsdb/get-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_LOG_LIST,
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Create.php
new file mode 100644
index 0000000000..9341779dcd
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Create.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb/transactions')
+ ->desc('Create transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'createTransaction',
+ description: '/docs/references/documentsdb/create-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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')
+ ->inject('user')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php
new file mode 100644
index 0000000000..036f2e9600
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php
@@ -0,0 +1,55 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/documentsdb/transactions/:transactionId')
+ ->desc('Delete transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'deleteTransaction',
+ description: '/docs/references/documentsdb/delete-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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')
+ ->inject('queueForDeletes')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Get.php
new file mode 100644
index 0000000000..7def4f0b9a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Get.php
@@ -0,0 +1,54 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/transactions/:transactionId')
+ ->desc('Get transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'getTransaction',
+ description: '/docs/references/documentsdb/get-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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/DocumentsDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Operations/Create.php
new file mode 100644
index 0000000000..bc15d440d1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Operations/Create.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/documentsdb/transactions/:transactionId/operations')
+ ->desc('Create operations')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'createOperations',
+ description: '/docs/references/documentsdb/create-operations.md',
+ auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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: 'documentsdb')), 'Array of staged operations.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
new file mode 100644
index 0000000000..b4c0c2ffab
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
@@ -0,0 +1,69 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/documentsdb/transactions/:transactionId')
+ ->desc('Update transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'updateTransaction',
+ description: '/docs/references/documentsdb/update-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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('project')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('user')
+ ->inject('transactionState')
+ ->inject('queueForDeletes')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('authorization')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/XList.php
new file mode 100644
index 0000000000..b216ce6a4a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/XList.php
@@ -0,0 +1,54 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/transactions')
+ ->desc('List transactions')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'transactions',
+ name: 'listTransactions',
+ description: '/docs/references/documentsdb/list-transactions.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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/Http/DocumentsDB/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Update.php
new file mode 100644
index 0000000000..4bf5747b54
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Update.php
@@ -0,0 +1,58 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/documentsdb/:databaseId')
+ ->desc('Update database')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].update')
+ ->label('audits.event', 'database.update')
+ ->label('audits.resource', 'database/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'documentsdb',
+ name: 'update',
+ description: '/docs/references/documentsdb/update.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('name', null, new Text(128), 'Database name. Max length: 128 chars.')
+ ->param('enabled', true, new Boolean(), 'Is database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php
new file mode 100644
index 0000000000..8373b6bc20
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/Get.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/:databaseId/usage')
+ ->desc('Get DocumentsDB usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: null,
+ name: 'getUsage',
+ description: '/docs/references/documentsdb/get-database-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_USAGE_DOCUMENTSDB,
+ )
+ ],
+ contentType: ContentType::JSON,
+ ),
+ ])
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php
new file mode 100644
index 0000000000..16535765ca
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Usage/XList.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb/usage')
+ ->desc('Get DocumentsDB usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'documentsDB',
+ group: null,
+ name: 'listUsage',
+ description: '/docs/references/documentsdb/list-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_USAGE_DATABASES,
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/XList.php
new file mode 100644
index 0000000000..13814b37e2
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/XList.php
@@ -0,0 +1,53 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/documentsdb')
+ ->desc('List databases')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'documentsDB',
+ group: 'documentsdb',
+ name: 'list',
+ description: '/docs/references/documentsdb/list.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE_LIST,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('queries', [], new Databases(), '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. You may filter on the following columns: ' . implode(', ', Databases::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Create.php
index 8aa6e1e28b..eb2293dc28 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Create.php
@@ -50,8 +50,10 @@ class Create extends DatabaseCreate
->param('databaseId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique 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.', false, ['dbForProject'])
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('project')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->callback($this->action(...));
}
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 9d32166a26..48f1136b09 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php
@@ -67,6 +67,7 @@ class Create extends CollectionCreate
->param('indexes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of column keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
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 aa5b94c00f..97c5465fe3 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php
@@ -54,6 +54,7 @@ class Delete extends CollectionDelete
->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
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 8186e07d61..e683aafba1 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
@@ -64,6 +64,7 @@ class Create extends IndexCreate
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
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 adaf83ccf1..37a3db01db 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
@@ -61,6 +61,7 @@ class Delete extends DocumentsDelete
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
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 d706d1f28b..bb839b752e 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
@@ -63,6 +63,7 @@ class Update extends DocumentsUpdate
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
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 58da5064f9..364bf4a928 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
@@ -63,6 +63,7 @@ class Upsert extends DocumentsUpsert
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
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 e1e717e9b1..2670cc00aa 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
@@ -65,6 +65,7 @@ class Decrement extends DecrementDocumentAttribute
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('plan')
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 0b20450254..ca6589aa3a 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
@@ -65,6 +65,7 @@ class Increment extends IncrementDocumentAttribute
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('plan')
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 fde8005d2b..26649accfb 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
@@ -104,6 +104,7 @@ class Create extends DocumentCreate
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('user')
->inject('queueForEvents')
->inject('usage')
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 1845edc307..addc87f610 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
@@ -67,6 +67,7 @@ class Delete extends DocumentDelete
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
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 43b799e5b1..48a24e9ec4 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
@@ -57,6 +57,7 @@ class Get extends DocumentGet
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('transactionState')
->inject('authorization')
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 a5f4787b05..e1d821130f 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
@@ -50,6 +50,7 @@ class XList extends DocumentLogXList
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
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 c0d90f9531..99599bd169 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
@@ -65,6 +65,7 @@ class Update extends DocumentUpdate
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
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 7f0aa0ad7d..472a49cf64 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
@@ -68,6 +68,7 @@ class Upsert extends DocumentUpsert
->inject('response')
->inject('user')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('usage')
->inject('transactionState')
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 6e5dcd9370..ca83b10aae 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
@@ -61,6 +61,7 @@ class XList extends DocumentXList
->inject('response')
->inject('dbForProject')
->inject('user')
+ ->inject('getDatabasesDB')
->inject('usage')
->inject('transactionState')
->inject('authorization')
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 c525f97715..88b16d57f0 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Update.php
@@ -62,6 +62,7 @@ class Update extends CollectionUpdate
->param('enabled', true, new Boolean(), 'Is table enabled? When set to \'disabled\', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
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 4261ceaab6..6976be014c 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
@@ -54,6 +54,7 @@ class Get extends CollectionUsageGet
->inject('response')
->inject('dbForProject')
->inject('authorization')
+ ->inject('getDatabasesDB')
->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
index 68ea2b8901..872927d533 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
@@ -51,8 +51,10 @@ class Update extends TransactionsUpdate
->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject'])
->param('commit', false, new Boolean(), 'Commit transaction?', true)
->param('rollback', false, new Boolean(), 'Rollback transaction?', true)
+ ->inject('project')
->inject('response')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
->inject('queueForDeletes')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/XList.php
index 80a9bd3686..8dc0f6521a 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/XList.php
@@ -9,6 +9,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Databases;
use Appwrite\Utopia\Response as UtopiaResponse;
+use Utopia\Database\Query;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
@@ -20,6 +21,16 @@ class XList extends DatabaseXList
return 'listTablesDatabases';
}
+ protected function getDatabaseTypeQueryFilters(): array
+ {
+ return [
+ Query::or([
+ Query::equal('type', [DATABASE_TYPE_TABLESDB, DATABASE_TYPE_LEGACY]),
+ Query::isNull('type'),
+ ]),
+ ];
+ }
+
public function __construct()
{
$this
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
new file mode 100644
index 0000000000..b85a8b30b4
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
@@ -0,0 +1,208 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections')
+ ->desc('Create collection')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].create')
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'collection.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'collections',
+ name: 'createCollection',
+ description: '/docs/references/vectorsdb/create-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique 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.', false, ['dbForProject'])
+ ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
+ ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimension.')
+ ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $databaseId, string $collectionId, string $name, int $dimension, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
+ {
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+
+ if ($database->isEmpty()) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+
+ $collectionId = $collectionId === 'unique()' ? ID::unique() : $collectionId;
+
+ // Map aggregate permissions into the multiple permissions they represent.
+ $permissions = Permission::aggregate($permissions) ?? [];
+
+ try {
+ $collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([
+ '$id' => $collectionId,
+ 'databaseInternalId' => $database->getSequence(),
+ 'databaseId' => $databaseId,
+ '$permissions' => $permissions,
+ 'documentSecurity' => $documentSecurity,
+ 'enabled' => $enabled,
+ 'name' => $name,
+ 'dimension' => $dimension,
+ 'search' => \implode(' ', [$collectionId, $name]),
+ ]));
+
+ } catch (DuplicateException) {
+ throw new Exception($this->getDuplicateException());
+ } catch (LimitException) {
+ throw new Exception($this->getLimitException());
+ } catch (NotFoundException) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+ /** @var Database $dbForDatabases */
+ $dbForDatabases = $getDatabasesDB($database);
+
+ $attributes = [];
+ $indexes = [];
+ $collections = (Config::getParam('collections', [])['vectorsdb'] ?? [])['collections'] ?? [];
+ foreach ($collections['defaultAttributes'] as $attribute) {
+ if ($attribute['$id'] === 'embeddings') {
+ $attribute['size'] = $dimension;
+ }
+ $attributes[] = new Document($attribute);
+ }
+ foreach ($collections['defaultIndexes'] as $index) {
+ $indexes[] = new Document($index);
+ }
+ try {
+ // passing null in creates only creates the metadata collection
+ if (!$dbForDatabases->exists(null, Database::METADATA)) {
+ $dbForDatabases->create();
+ }
+ $dbForDatabases->createCollection(
+ id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
+ permissions: $permissions,
+ documentSecurity: $documentSecurity,
+ attributes:$attributes,
+ indexes:$indexes
+ );
+ // Create attribute and indexes metadata documents in the attributes and indexes collections
+ // needed for the get and list calls
+ $attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimension) {
+ $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id'];
+ return new Document([
+ '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
+ 'key' => $key,
+ 'databaseInternalId' => $database->getSequence(),
+ 'databaseId' => $databaseId,
+ 'collectionInternalId' => $collection->getSequence(),
+ 'collectionId' => $collectionId,
+ 'type' => $attributeConfig['type'],
+ 'status' => 'available',
+ 'size' => $dimension,
+ 'required' => $attributeConfig['required'] ?? false,
+ 'signed' => $attributeConfig['signed'] ?? false,
+ 'default' => $attributeConfig['default'] ?? null,
+ 'array' => $attributeConfig['array'] ?? false,
+ 'format' => $attributeConfig['format'] ?? '',
+ 'formatOptions' => $attributeConfig['formatOptions'] ?? [],
+ 'filters' => $attributeConfig['filters'] ?? [],
+ 'options' => $attributeConfig['options'] ?? [],
+ ]);
+ }, $collections['defaultAttributes']);
+ $dbForProject->createDocuments('attributes', $attributeDocs);
+
+ $indexDocs = array_map(function ($indexConfig) use ($database, $collection, $databaseId, $collectionId) {
+ $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id'];
+
+ return new Document([
+ '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
+ 'key' => $key,
+ 'status' => 'available',
+ 'databaseInternalId' => $database->getSequence(),
+ 'databaseId' => $databaseId,
+ 'collectionInternalId' => $collection->getSequence(),
+ 'collectionId' => $collectionId,
+ 'type' => $indexConfig['type'],
+ 'attributes' => $indexConfig['attributes'] ?? [],
+ 'lengths' => $indexConfig['lengths'] ?? [],
+ 'orders' => $indexConfig['orders'] ?? [],
+ ]);
+ }, $collections['defaultIndexes']);
+
+ if (!empty($indexDocs)) {
+ $dbForProject->createDocuments('indexes', $indexDocs);
+ }
+ } catch (DuplicateException) {
+ throw new Exception($this->getDuplicateException());
+ } catch (IndexException) {
+ throw new Exception($this->getInvalidIndexException());
+ } catch (LimitException) {
+ throw new Exception($this->getLimitException());
+ }
+
+ $queueForEvents
+ ->setContext('database', $database)
+ ->setParam('databaseId', $databaseId)
+ ->setParam($this->getEventsParamKey(), $collection->getId());
+
+ $response
+ ->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
+ ->dynamic($collection, $this->getResponseModel());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php
new file mode 100644
index 0000000000..f1188868aa
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php
@@ -0,0 +1,62 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId')
+ ->desc('Delete collection')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].delete')
+ ->label('audits.event', 'collection.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'collections',
+ name: 'deleteCollection',
+ description: '/docs/references/vectorsdb/delete-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
new file mode 100644
index 0000000000..a4d640b423
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
@@ -0,0 +1,72 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Delete documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteDocuments',
+ description: '/docs/references/vectorsdb/delete-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
new file mode 100644
index 0000000000..2784fa220a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
@@ -0,0 +1,74 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Update documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'documents.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'updateDocuments',
+ description: '/docs/references/vectorsdb/update-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
new file mode 100644
index 0000000000..cfbf6c9158
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
@@ -0,0 +1,74 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Upsert documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'upsertDocuments',
+ description: '/docs/references/vectorsdb/upsert-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ )
+ ])
+ ->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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('queueForEvents')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
new file mode 100644
index 0000000000..563b5f60ef
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
@@ -0,0 +1,116 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('Create document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'createDocument',
+ desc: 'Create document',
+ description: '/docs/references/vectorsdb/create-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ parameters: [
+ new Parameter('databaseId', optional: false),
+ new Parameter('collectionId', optional: false),
+ new Parameter('documentId', optional: false),
+ new Parameter('data', optional: false),
+ new Parameter('permissions', optional: true),
+ ]
+ ),
+ new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'createDocuments',
+ desc: 'Create documents',
+ description: '/docs/references/vectorsdb/create-documents.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getBulkResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ parameters: [
+ new Parameter('databaseId', optional: false),
+ new Parameter('collectionId', optional: false),
+ new Parameter('documents', optional: false),
+ ]
+ )
+ ])
+ ->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('data', [], new JSON(), 'Document data as JSON object.', true, example: '{"embeddings": [0.12, -0.55, 0.88, 1.02], "metadata": {"key":"value"} }')
+ ->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('getDatabasesDB')
+ ->inject('user')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('plan')
+ ->inject('authorization')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Delete.php
new file mode 100644
index 0000000000..eca6049970
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Delete.php
@@ -0,0 +1,76 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Delete document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
+ ->label('audits.event', 'document.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteDocument',
+ description: '/docs/references/vectorsdb/delete-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->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')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Get.php
new file mode 100644
index 0000000000..2a7090a01e
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Get.php
@@ -0,0 +1,64 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Get document')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'getDocument',
+ description: '/docs/references/vectorsdb/get-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('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('getDatabasesDB')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php
new file mode 100644
index 0000000000..dea9d30119
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php
@@ -0,0 +1,59 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
+ ->desc('List document logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'logs',
+ name: 'listDocumentLogs',
+ description: '/docs/references/vectorsdb/get-document-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Update.php
new file mode 100644
index 0000000000..2624cc82e4
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Update.php
@@ -0,0 +1,75 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Update document')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'updateDocument',
+ description: '/docs/references/vectorsdb/update-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', new UID(), 'Document ID.')
+ ->param('data', [], new JSON(), 'Document data as JSON object. Include only fields 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')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Upsert.php
new file mode 100644
index 0000000000..f8f17d33d9
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Upsert.php
@@ -0,0 +1,79 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId')
+ ->desc('Upsert a document')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'document.upsert')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'upsertDocument',
+ description: '/docs/references/vectorsdb/upsert-document.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Document ID.', false, ['dbForProject'])
+ ->param('data', [], new JSON(), 'Document data as JSON object. Include all required fields of the document 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')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/XList.php
new file mode 100644
index 0000000000..c9ed05ac02
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/XList.php
@@ -0,0 +1,68 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents')
+ ->desc('List documents')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'listDocuments',
+ description: '/docs/references/vectorsdb/list-documents.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('user')
+ ->inject('getDatabasesDB')
+ ->inject('usage')
+ ->inject('transactionState')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Get.php
new file mode 100644
index 0000000000..9619bb5048
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Get.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId')
+ ->desc('Get collection')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'collections',
+ name: 'getCollection',
+ description: '/docs/references/vectorsdb/get-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php
new file mode 100644
index 0000000000..a535dd5724
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php
@@ -0,0 +1,73 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/indexes')
+ ->desc('Create index')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create')
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'index.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.tableId}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'createIndex',
+ description: '/docs/references/vectorsdb/create-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_ACCEPTED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('key', null, new Key(), 'Index Key.')
+ ->param('type', null, new WhiteList([Database::INDEX_HNSW_EUCLIDEAN,Database::INDEX_HNSW_DOT, Database::INDEX_HNSW_COSINE, Database::INDEX_OBJECT, Database::INDEX_KEY, Database::INDEX_UNIQUE]), 'Index type.')
+ ->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
+ ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
+ ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php
new file mode 100644
index 0000000000..5c7fc47ee0
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php
@@ -0,0 +1,67 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/indexes/:key')
+ ->desc('Delete index')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update')
+ ->label('audits.event', 'index.delete')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'deleteIndex', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/vectorsdb/delete-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->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('key', '', new Key(), 'Index Key.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Get.php
new file mode 100644
index 0000000000..4cf646acba
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Get.php
@@ -0,0 +1,58 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/indexes/:key')
+ ->desc('Get index')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'getIndex', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/vectorsdb/get-index.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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('key', null, new Key(), 'Index Key.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/XList.php
new file mode 100644
index 0000000000..acc46fb570
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/XList.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/indexes')
+ ->desc('List indexes')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'listIndexes', // getName needs to be different from parent action to avoid conflict in path name
+ description: '/docs/references/vectorsdb/list-indexes.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->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 Indexes(), '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. You may filter on the following attributes: ' . implode(', ', Indexes::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php
new file mode 100644
index 0000000000..cd0e45eb47
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php
@@ -0,0 +1,57 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
+ ->desc('List collection logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'listCollectionLogs',
+ description: '/docs/references/vectorsdb/get-collection-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Update.php
new file mode 100644
index 0000000000..f8ba767e7e
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Update.php
@@ -0,0 +1,117 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId')
+ ->desc('Update collection')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].update')
+ ->label('audits.event', 'collection.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'collections',
+ name: 'updateCollection',
+ description: '/docs/references/vectorsdb/update-collection.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_VECTORSDB_COLLECTION,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
+ ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimensions.', true)
+ ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
+ ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $databaseId, string $collectionId, ?string $name, ?int $dimensions, ?array $permissions, bool $documentSecurity, ?bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
+ {
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+ if ($database->isEmpty()) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+
+ $collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
+ if ($collection->isEmpty()) {
+ throw new Exception($this->getNotFoundException());
+ }
+
+ $permissions ??= $collection->getPermissions();
+
+ // Map aggregate permissions into the multiple permissions they represent.
+ $permissions = Permission::aggregate($permissions);
+
+ $enabled ??= $collection->getAttribute('enabled', true);
+
+ $updated = $dbForProject->updateDocument(
+ 'database_' . $database->getSequence(),
+ $collectionId,
+ $collection
+ ->setAttribute('name', $name ?? $collection->getAttribute('name'))
+ ->setAttribute('dimension', $dimensions ?? $collection->getAttribute('dimension'))
+ ->setAttribute('$permissions', $permissions)
+ ->setAttribute('documentSecurity', $documentSecurity)
+ ->setAttribute('enabled', $enabled)
+ ->setAttribute('search', \implode(' ', [$collectionId, $name ?? $collection->getAttribute('name')]))
+ );
+
+ $dbForDatabases = $getDatabasesDB($database);
+ $dbForDatabases->updateCollection('database_' . $database->getSequence() . '_collection_' . $updated->getSequence(), $permissions, $documentSecurity);
+
+ $queueForEvents
+ ->setContext('database', $database)
+ ->setParam('databaseId', $databaseId)
+ ->setParam($this->getEventsParamKey(), $updated->getId());
+
+ $response->dynamic($updated, $this->getResponseModel());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php
new file mode 100644
index 0000000000..7e0f79a9f1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Usage/Get.php
@@ -0,0 +1,64 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/usage')
+ ->desc('Get collection usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: null,
+ name: 'getCollectionUsage',
+ description: '/docs/references/vectorsdb/get-collection-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->inject('getDatabasesDB')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/XList.php
new file mode 100644
index 0000000000..7ba26b8b6a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/XList.php
@@ -0,0 +1,61 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/collections')
+ ->desc('List collections')
+ ->groups(['api', 'database'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'collections',
+ name: 'listCollections',
+ description: '/docs/references/vectorsdb/list-collections.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('queries', [], new Collections(), '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. You may filter on the following attributes: ' . implode(', ', Collections::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Create.php
new file mode 100644
index 0000000000..cc2914fc10
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Create.php
@@ -0,0 +1,59 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb')
+ ->desc('Create database')
+ ->groups(['api', 'database'])
+ ->label('event', 'databases.[databaseId].create')
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('audits.event', 'database.create')
+ ->label('audits.resource', 'database/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'vectorsdb',
+ name: 'create',
+ description: '/docs/references/vectorsdb/create.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_CREATED,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new CustomId(), 'Unique 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.')
+ ->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
+ ->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('project')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php
new file mode 100644
index 0000000000..c9d36904a9
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php
@@ -0,0 +1,55 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/:databaseId')
+ ->desc('Delete database')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].delete')
+ ->label('audits.event', 'database.delete')
+ ->label('audits.resource', 'database/{request.databaseId}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'vectorsdb',
+ name: 'delete',
+ description: '/docs/references/vectorsdb/delete.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_NOCONTENT,
+ model: UtopiaResponse::MODEL_NONE,
+ )
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForDatabase')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php
new file mode 100644
index 0000000000..d9b378774b
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Embeddings/Text/Create.php
@@ -0,0 +1,152 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/embeddings/text')
+ ->desc('Create Text Embeddings')
+ ->groups(['api', 'database'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_EMBEDDINGS_TEXT)
+ ->label('audits.event', 'embedding.create')
+ ->label('audits.resource', 'vectorsdb/embeddings/text')
+ ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: $this->getSdkGroup(),
+ name: 'createTextEmbeddings',
+ desc: 'Create Text Embedding',
+ description: '/docs/references/vectorsdb/create-document.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getBulkResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ parameters: [
+ new Parameter('texts', optional: false),
+ new Parameter('model', optional: true),
+ ]
+ )
+ ])
+ ->param('texts', [], fn (array $plan) => new ArrayList(new Text(0), $plan['databasesMaxEmbeddingTexts'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of text to generate embeddings.', false, ['plan'])
+ ->param('model', Ollama::MODEL_EMBEDDING_GEMMA, new WhiteList(Ollama::MODELS), 'The embedding model to use for generating vector embeddings.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('embeddingAgent')
+ ->inject('usage')
+ ->inject('log')
+ ->inject('logger')
+ ->callback($this->action(...));
+ }
+
+ public function action(array $texts, string $model, UtopiaResponse $response, Document $project, Agent $embeddingAgent, Context $usage, Log $log, ?Logger $logger): void
+ {
+ $results = [];
+ $embeddingAgent->getAdapter()->setModel($model);
+ $dimension = $embeddingAgent->getAdapter()->getEmbeddingDimension();
+
+ $totalDuration = 0;
+ $totalTokens = 0;
+ $totalErrors = 0;
+ foreach ($texts as $text) {
+ $embedding = [];
+ $error = '';
+ try {
+ $embedResult = $embeddingAgent->embed($text);
+ $embedding = $embedResult['embedding'] ?? [];
+ $totalDuration += $embedResult['totalDuration'] ?? 0;
+ $totalTokens += $embedResult['tokensProcessed'] ?? 0;
+ } catch (\Exception $e) {
+ $error = 'Error while generating embedding';
+ $totalErrors += 1;
+ if ($logger) {
+ $log->setNamespace("http");
+ $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
+ $log->setVersion(System::getEnv('_APP_VERSION', 'UNKNOWN'));
+ $log->setType(Log::TYPE_ERROR);
+ $log->setMessage($e->getMessage());
+
+ $log->addTag('embeddingModel', $model);
+ $log->addTag('code', $e->getCode());
+ $log->addTag('projectId', $project->getId());
+
+ $log->addExtra('file', $e->getFile());
+ $log->addExtra('line', $e->getLine());
+ $log->addExtra('trace', $e->getTraceAsString());
+
+ $logger->addLog($log);
+ }
+ }
+
+ $results[] = new Document([
+ 'model' => $model,
+ 'dimension' => $dimension,
+ 'embedding' => $embedding,
+ 'error' => $error
+ ]);
+ }
+ $embeddings = new Document([
+ 'embeddings' => $results,
+ 'total' => \count($results),
+ ]);
+
+ $response
+ ->setStatusCode(SwooleResponse::STATUS_CODE_OK)
+ ->dynamic($embeddings, $this->getBulkResponseModel());
+
+ $usage
+ ->addMetric(METRIC_EMBEDDINGS_TEXT, \count($texts))
+ ->addMetric(\str_replace('{embeddingModel}', $model, METRIC_EMBEDDINGS_MODEL_TEXT), \count($texts))
+ ->addMetric(METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS, $totalTokens)
+ ->addMetric(\str_replace('{embeddingModel}', $model, METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_TOKENS), $totalTokens)
+ ->addMetric(METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION, $totalDuration)
+ ->addMetric(\str_replace('{embeddingModel}', $model, METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_DURATION), $totalDuration)
+ ->addMetric(METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR, $totalErrors)
+ ->addMetric(\str_replace('{embeddingModel}', $model, METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_ERROR), $totalErrors);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Get.php
new file mode 100644
index 0000000000..a79632b105
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Get.php
@@ -0,0 +1,49 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId')
+ ->desc('Get database')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'vectorsdb',
+ name: 'get',
+ description: '/docs/references/vectorsdb/get.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Logs/XList.php
new file mode 100644
index 0000000000..d8c1df5f04
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Logs/XList.php
@@ -0,0 +1,59 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/logs')
+ ->desc('List database logs')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: 'logs',
+ name: 'listDatabaseLogs',
+ description: '/docs/references/vectorsdb/get-logs.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_LOG_LIST,
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->inject('authorization')
+ ->inject('audit')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Create.php
new file mode 100644
index 0000000000..cb67d3f7f1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Create.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/transactions')
+ ->desc('Create transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'createTransaction',
+ description: '/docs/references/vectorsdb/create-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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')
+ ->inject('user')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php
new file mode 100644
index 0000000000..0ac2caecba
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php
@@ -0,0 +1,55 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/vectorsdb/transactions/:transactionId')
+ ->desc('Delete transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'deleteTransaction',
+ description: '/docs/references/vectorsdb/delete-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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')
+ ->inject('queueForDeletes')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Get.php
new file mode 100644
index 0000000000..fa4cc86cdd
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Get.php
@@ -0,0 +1,54 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/transactions/:transactionId')
+ ->desc('Get transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'getTransaction',
+ description: '/docs/references/vectorsdb/get-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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/VectorsDB/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Operations/Create.php
new file mode 100644
index 0000000000..830c0c3fe1
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Operations/Create.php
@@ -0,0 +1,60 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/vectorsdb/transactions/:transactionId/operations')
+ ->desc('Create operations')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'createOperations',
+ description: '/docs/references/vectorsdb/create-operations.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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: 'documentsdb')), 'Array of staged operations.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('transactionState')
+ ->inject('plan')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
new file mode 100644
index 0000000000..f4bd4d67f5
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
@@ -0,0 +1,69 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/vectorsdb/transactions/:transactionId')
+ ->desc('Update transaction')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'updateTransaction',
+ description: '/docs/references/vectorsdb/update-transaction.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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('project')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('getDatabasesDB')
+ ->inject('user')
+ ->inject('transactionState')
+ ->inject('queueForDeletes')
+ ->inject('queueForEvents')
+ ->inject('usage')
+ ->inject('queueForRealtime')
+ ->inject('queueForFunctions')
+ ->inject('queueForWebhooks')
+ ->inject('authorization')
+ ->inject('eventProcessor')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/XList.php
new file mode 100644
index 0000000000..fb95667ffb
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/XList.php
@@ -0,0 +1,54 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/transactions')
+ ->desc('List transactions')
+ ->groups(['api', 'database', 'transactions'])
+ ->label('scope', 'documents.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'transactions',
+ name: 'listTransactions',
+ description: '/docs/references/vectorsdb/list-transactions.md',
+ auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
+ 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/Http/VectorsDB/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Update.php
new file mode 100644
index 0000000000..0b10d6d98b
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Update.php
@@ -0,0 +1,57 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/vectorsdb/:databaseId')
+ ->desc('Update database')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'databases.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].update')
+ ->label('audits.event', 'database.update')
+ ->label('audits.resource', 'database/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'vectorsdb',
+ name: 'update',
+ description: '/docs/references/vectorsdb/update.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('name', null, new Text(128), 'Database name. Max length: 128 chars.')
+ ->param('enabled', true, new Boolean(), 'Is database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php
new file mode 100644
index 0000000000..051e2e39fa
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/Get.php
@@ -0,0 +1,59 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/:databaseId/usage')
+ ->desc('Get VectorsDB usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: null,
+ name: 'getUsage',
+ description: '/docs/references/vectorsdb/get-database-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_USAGE_VECTORSDB,
+ )
+ ],
+ contentType: ContentType::JSON,
+ ),
+ ])
+ ->param('databaseId', '', new UID(), 'Database ID.')
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php
new file mode 100644
index 0000000000..d91a5963c4
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Usage/XList.php
@@ -0,0 +1,56 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb/usage')
+ ->desc('Get VectorsDB usage stats')
+ ->groups(['api', 'database', 'usage'])
+ ->label('scope', 'collections.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', [
+ new Method(
+ namespace: 'vectorsDB',
+ group: null,
+ name: 'listUsage',
+ description: '/docs/references/vectorsdb/list-usage.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_USAGE_VECTORSDBS,
+ )
+ ],
+ contentType: ContentType::JSON
+ ),
+ ])
+ ->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/XList.php
new file mode 100644
index 0000000000..e18a89c6a4
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/XList.php
@@ -0,0 +1,53 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/vectorsdb')
+ ->desc('List databases')
+ ->groups(['api', 'database'])
+ ->label('scope', 'databases.read')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('sdk', new Method(
+ namespace: 'vectorsDB',
+ group: 'vectorsdb',
+ name: 'list',
+ description: '/docs/references/vectorsdb/list.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: UtopiaResponse::MODEL_DATABASE_LIST,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('queries', [], new Databases(), '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. You may filter on the following columns: ' . implode(', ', Databases::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', 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 f683f537bc..5146382b56 100644
--- a/src/Appwrite/Platform/Modules/Databases/Services/Http.php
+++ b/src/Appwrite/Platform/Modules/Databases/Services/Http.php
@@ -3,8 +3,10 @@
namespace Appwrite\Platform\Modules\Databases\Services;
use Appwrite\Platform\Modules\Databases\Http\Init\Timeout;
+use Appwrite\Platform\Modules\Databases\Services\Registry\DocumentsDB as DocumentsDBRegistry;
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\TablesDB as TablesDBDBRegistry;
+use Appwrite\Platform\Modules\Databases\Services\Registry\VectorsDB as VectorsDBRegistry;
use Utopia\Platform\Service;
class Http extends Service
@@ -17,7 +19,9 @@ class Http extends Service
foreach ([
LegacyRegistry::class,
- TablesDBRegistry::class,
+ TablesDBDBRegistry::class,
+ DocumentsDBRegistry::class,
+ VectorsDBRegistry::class
] as $registrar) {
new $registrar($this);
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php
new file mode 100644
index 0000000000..a1e3538cac
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php
@@ -0,0 +1,109 @@
+registerDatabaseActions($service);
+ $this->registerTableActions($service);
+ $this->registerIndexActions($service);
+ $this->registerRowActions($service);
+ $this->registerTransactionActions($service);
+ }
+
+ private function registerDatabaseActions(Service $service): void
+ {
+ $service->addAction(CreateTablesDatabase::getName(), new CreateTablesDatabase());
+ $service->addAction(GetTablesDatabase::getName(), new GetTablesDatabase());
+ $service->addAction(UpdateTablesDatabase::getName(), new UpdateTablesDatabase());
+ $service->addAction(DeleteTablesDatabase::getName(), new DeleteTablesDatabase());
+ $service->addAction(ListTablesDatabase::getName(), new ListTablesDatabase());
+ $service->addAction(GetTablesDatabaseUsage::getName(), new GetTablesDatabaseUsage());
+ $service->addAction(ListTablesDatabaseUsage::getName(), new ListTablesDatabaseUsage());
+ }
+
+ private function registerTableActions(Service $service): void
+ {
+ $service->addAction(CreateTable::getName(), new CreateTable());
+ $service->addAction(GetTable::getName(), new GetTable());
+ $service->addAction(UpdateTable::getName(), new UpdateTable());
+ $service->addAction(DeleteTable::getName(), new DeleteTable());
+ $service->addAction(ListTables::getName(), new ListTables());
+ $service->addAction(ListTableLogs::getName(), new ListTableLogs());
+ $service->addAction(GetTableUsage::getName(), new GetTableUsage());
+ }
+
+ private function registerIndexActions(Service $service): void
+ {
+ $service->addAction(CreateColumnIndex::getName(), new CreateColumnIndex());
+ $service->addAction(GetColumnIndex::getName(), new GetColumnIndex());
+ $service->addAction(DeleteColumnIndex::getName(), new DeleteColumnIndex());
+ $service->addAction(ListColumnIndexes::getName(), new ListColumnIndexes());
+ }
+
+ private function registerRowActions(Service $service): void
+ {
+ $service->addAction(CreateRow::getName(), new CreateRow());
+ $service->addAction(GetRow::getName(), new GetRow());
+ $service->addAction(UpdateRow::getName(), new UpdateRow());
+ $service->addAction(UpdateRows::getName(), new UpdateRows());
+ $service->addAction(UpsertRow::getName(), new UpsertRow());
+ $service->addAction(UpsertRows::getName(), new UpsertRows());
+ $service->addAction(DeleteRow::getName(), new DeleteRow());
+ $service->addAction(DeleteRows::getName(), new DeleteRows());
+ $service->addAction(ListRows::getName(), new ListRows());
+ $service->addAction(ListRowLogs::getName(), new ListRowLogs());
+ $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/VectorsDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php
new file mode 100644
index 0000000000..5d12b14b1a
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php
@@ -0,0 +1,112 @@
+registerDatabaseActions($service);
+ $this->registerCollectionActions($service);
+ $this->registerIndexActions($service);
+ $this->registerDocumentActions($service);
+ $this->registerEmbeddingActions($service);
+ $this->registerTransactionActions($service);
+ }
+
+ private function registerDatabaseActions(Service $service): void
+ {
+ $service->addAction(CreateVectorDatabase::getName(), new CreateVectorDatabase());
+ $service->addAction(GetVectorDatabase::getName(), new GetVectorDatabase());
+ $service->addAction(UpdateVectorDatabase::getName(), new UpdateVectorDatabase());
+ $service->addAction(DeleteVectorDatabase::getName(), new DeleteVectorDatabase());
+ $service->addAction(ListVectorDatabases::getName(), new ListVectorDatabases());
+ $service->addAction(GetVectorDatabaseUsage::getName(), new GetVectorDatabaseUsage());
+ $service->addAction(ListVectorDatabaseUsage::getName(), new ListVectorDatabaseUsage());
+ }
+
+ private function registerCollectionActions(Service $service): void
+ {
+ $service->addAction(CreateCollection::getName(), new CreateCollection());
+ $service->addAction(GetCollection::getName(), new GetCollection());
+ $service->addAction(UpdateCollection::getName(), new UpdateCollection());
+ $service->addAction(DeleteCollection::getName(), new DeleteCollection());
+ $service->addAction(ListCollections::getName(), new ListCollections());
+ $service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
+ $service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
+ }
+
+ private function registerIndexActions(Service $service): void
+ {
+ $service->addAction(CreateIndex::getName(), new CreateIndex());
+ $service->addAction(GetIndex::getName(), new GetIndex());
+ $service->addAction(DeleteIndex::getName(), new DeleteIndex());
+ $service->addAction(ListIndexes::getName(), new ListIndexes());
+ }
+
+ private function registerDocumentActions(Service $service): void
+ {
+ $service->addAction(CreateDocument::getName(), new CreateDocument());
+ $service->addAction(UpdateDocument::getName(), new UpdateDocument());
+ $service->addAction(UpsertDocument::getName(), new UpsertDocument());
+ $service->addAction(GetDocument::getName(), new GetDocument());
+ $service->addAction(ListDocuments::getName(), new ListDocuments());
+ $service->addAction(DeleteDocument::getName(), new DeleteDocument());
+ $service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
+ $service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
+ $service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
+ $service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
+ }
+
+ 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());
+ }
+
+ private function registerEmbeddingActions(Service $service): void
+ {
+ $service->addAction(CreateTextEmbeddings::getName(), new CreateTextEmbeddings());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
index 60d70b7942..66ed3e0eab 100644
--- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
+++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
@@ -36,6 +36,7 @@ class Databases extends Action
->inject('project')
->inject('dbForPlatform')
->inject('dbForProject')
+ ->inject('getDatabasesDB')
->inject('queueForRealtime')
->inject('log')
->callback($this->action(...));
@@ -51,7 +52,7 @@ class Databases extends Action
* @return void
* @throws \Exception
*/
- public function action(Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime, Log $log): void
+ public function action(Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, callable $getDatabasesDB, Realtime $queueForRealtime, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -63,7 +64,10 @@ class Databases extends Action
$document = new Document($payload['row'] ?? $payload['document'] ?? []);
$collection = new Document($payload['table'] ?? $payload['collection'] ?? []);
$database = new Document($payload['database'] ?? []);
-
+ /**
+ * @var Database $dbForDatabases
+ */
+ $dbForDatabases = $getDatabasesDB($database);
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
@@ -74,12 +78,12 @@ class Databases extends Action
$log->addTag('databaseId', $database->getId());
match (\strval($type)) {
- DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $dbForProject),
- DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $dbForProject),
+ DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $dbForProject, $dbForDatabases),
+ DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $dbForProject, $dbForDatabases),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
- DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
- DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
- DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
+ DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $dbForDatabases, $queueForRealtime),
+ DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $dbForDatabases, $queueForRealtime),
+ DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $dbForDatabases, $queueForRealtime),
default => throw new Exception('No database operation for type: ' . \strval($type)),
};
@@ -244,6 +248,7 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
+ * @param Database $dbForDatabases
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
@@ -251,7 +256,7 @@ class Databases extends Action
* @throws \Exception
* @throws \Throwable
**/
- private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
+ private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForDatabases, Database $dbForProject, Realtime $queueForRealtime): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection/table');
@@ -386,7 +391,7 @@ class Databases extends Action
}
if ($exists) { // Delete the duplicate if created, else update in db
- $this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime);
+ $this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $dbForDatabases, $queueForRealtime);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), new Document([
'attributes' => $index->getAttribute('attributes'),
@@ -415,6 +420,7 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
+ * @param Database $dbForDatabases
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
@@ -423,7 +429,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
- private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
+ private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Database $dbForDatabases, Realtime $queueForRealtime): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection/table');
@@ -443,7 +449,7 @@ class Databases extends Action
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
- if (!$dbForProject->createIndex('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $key, $type, $attributes, $lengths, $orders)) {
+ if (!$dbForDatabases->createIndex('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $key, $type, $attributes, $lengths, $orders)) {
throw new DatabaseException('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
@@ -473,6 +479,7 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
+ * @param Database $dbForDatabases
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
@@ -481,7 +488,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
- private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
+ private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Database $dbForDatabases, Realtime $queueForRealtime): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection/table');
@@ -497,7 +504,7 @@ class Databases extends Action
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
- if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $key)) {
+ if ($status !== 'failed' && !$dbForDatabases->deleteIndex('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $key)) {
throw new DatabaseException('Failed to delete index');
}
$dbForProject->deleteDocument('indexes', $index->getId());
@@ -525,14 +532,15 @@ class Databases extends Action
/**
* @param Document $database
- * @param $dbForProject
+ * @param Database $dbForProject
+ * @param Database $dbForDatabases
* @return void
* @throws Exception
*/
- protected function deleteDatabase(Document $database, $dbForProject): void
+ protected function deleteDatabase(Document $database, Database $dbForProject, Database $dbForDatabases): void
{
- $this->deleteByGroup('database_' . $database->getSequence(), [], $dbForProject, function ($collection) use ($database, $dbForProject) {
- $this->deleteCollection($database, $collection, $dbForProject);
+ $this->deleteByGroup('database_' . $database->getSequence(), [], $dbForProject, function ($collection) use ($database, $dbForProject, $dbForDatabases) {
+ $this->deleteCollection($database, $collection, $dbForProject, $dbForDatabases);
});
$dbForProject->deleteCollection('database_' . $database->getSequence());
@@ -542,6 +550,7 @@ class Databases extends Action
* @param Document $database
* @param Document $collection
* @param Database $dbForProject
+ * @param Database $dbForDatabases
* @return void
* @throws Authorization
* @throws Conflict
@@ -550,7 +559,7 @@ class Databases extends Action
* @throws Structure
* @throws Exception
*/
- protected function deleteCollection(Document $database, Document $collection, Database $dbForProject): void
+ protected function deleteCollection(Document $database, Document $collection, Database $dbForProject, Database $dbForDatabases): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection/table');
@@ -560,7 +569,7 @@ class Databases extends Action
$collectionInternalId = $collection->getSequence();
$databaseInternalId = $database->getSequence();
- $dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $collection->getSequence());
+ $dbForDatabases->deleteCollection('database_' . $databaseInternalId . '_collection_' . $collection->getSequence());
/**
* Related collections relating to current collection
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Init.php b/src/Appwrite/Platform/Modules/Project/Http/Init.php
new file mode 100644
index 0000000000..ff191ade6c
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Init.php
@@ -0,0 +1,32 @@
+setType(Action::TYPE_INIT)
+ ->groups(['project'])
+ ->inject('project')
+ ->callback(function (Document $project) {
+ if ($project->getId() === 'console') {
+ throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
+ }
+
+ if ($project->isEmpty()) {
+ throw new Exception(Exception::PROJECT_NOT_FOUND);
+ }
+ });
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php
new file mode 100644
index 0000000000..acc39bb68d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php
@@ -0,0 +1,108 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/project/variables')
+ ->desc('Create project variable')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.write')
+ ->label('event', 'variables.[variableId].create')
+ ->label('audits.event', 'project.variable.create')
+ ->label('audits.resource', 'project.variable/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'variables',
+ name: 'createVariable',
+ description: <<param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable 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.', false, ['dbForProject'])
+ ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.')
+ ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.')
+ ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
+ ->inject('response')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $variableId,
+ string $key,
+ string $value,
+ bool $secret,
+ Response $response,
+ QueueEvent $queueForEvents,
+ Database $dbForProject,
+ ) {
+ $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId;
+
+ $variable = new Document([
+ '$id' => $variableId,
+ '$permissions' => [],
+ 'resourceInternalId' => '', // Already in project DB anyway
+ 'resourceId' => '', // Already in project DB anyway
+ 'resourceType' => 'project',
+ 'key' => $key,
+ 'value' => $value,
+ 'secret' => $secret,
+ 'search' => implode(' ', [$variableId, $key, 'project']),
+ ]);
+
+ try {
+ $variable = $dbForProject->createDocument('variables', $variable);
+ } catch (DuplicateException $th) {
+ throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
+ }
+
+ foreach (['functions', 'sites'] as $collection) {
+ $dbForProject->updateDocuments($collection, new Document([
+ 'live' => false
+ ]));
+ }
+
+ $queueForEvents->setParam('variableId', $variable->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($variable, Response::MODEL_VARIABLE);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
new file mode 100644
index 0000000000..ac47ec3dbb
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
@@ -0,0 +1,88 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/project/variables/:variableId')
+ ->desc('Delete project variable')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.write')
+ ->label('event', 'variables.[variableId].delete')
+ ->label('audits.event', 'project.variable.delete')
+ ->label('audits.resource', 'project.variable/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'variables',
+ name: 'deleteVariable',
+ description: <<param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $variableId,
+ Response $response,
+ Database $dbForProject,
+ Event $queueForEvents,
+ ) {
+ $variable = $dbForProject->getDocument('variables', $variableId);
+
+ if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
+ throw new Exception(Exception::VARIABLE_NOT_FOUND);
+ }
+
+ if (!$dbForProject->deleteDocument('variables', $variable->getId())) {
+ throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
+ };
+
+ foreach (['functions', 'sites'] as $collection) {
+ $dbForProject->updateDocuments($collection, new Document([
+ 'live' => false
+ ]));
+ }
+
+ $queueForEvents->setParam('variableId', $variable->getId());
+
+ $response->noContent();
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php
new file mode 100644
index 0000000000..6de51dacaf
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php
@@ -0,0 +1,67 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/project/variables/:variableId')
+ ->desc('Get project variable')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.read')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'variables',
+ name: 'getVariable',
+ description: <<param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $variableId,
+ Response $response,
+ Database $dbForProject,
+ ) {
+ $variable = $dbForProject->getDocument('variables', $variableId);
+
+ if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
+ throw new Exception(Exception::VARIABLE_NOT_FOUND);
+ }
+
+ $response->dynamic($variable, Response::MODEL_VARIABLE);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php
new file mode 100644
index 0000000000..61a943b618
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php
@@ -0,0 +1,121 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
+ ->setHttpPath('/v1/project/variables/:variableId')
+ ->desc('Update project variable')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.write')
+ ->label('event', 'variables.[variableId].update')
+ ->label('audits.event', 'project.variable.update')
+ ->label('audits.resource', 'project.variable/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'variables',
+ name: 'updateVariable',
+ description: <<param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
+ ->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
+ ->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
+ ->inject('response')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $variableId,
+ ?string $key,
+ ?string $value,
+ ?bool $secret,
+ Response $response,
+ QueueEvent $queueForEvents,
+ Database $dbForProject,
+ ) {
+ $variable = $dbForProject->getDocument('variables', $variableId);
+
+ if ($variable->isEmpty() || $variable->getAttribute('resourceType', '') !== 'project') {
+ throw new Exception(Exception::VARIABLE_NOT_FOUND);
+ }
+
+ $isSecretVariable = $variable->getAttribute('secret', false) === true;
+ if ($isSecretVariable && $secret === false) {
+ throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
+ }
+
+ if (\is_null($key) && \is_null($value) && \is_null($secret)) {
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID);
+ }
+
+ $updates = new Document();
+
+ if (!\is_null($key)) {
+ $updates->setAttribute('key', $key);
+ $updates->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
+ }
+
+ if (!\is_null($value)) {
+ $updates->setAttribute('value', $value);
+ }
+
+ if (!\is_null($secret)) {
+ $updates->setAttribute('secret', $secret);
+ }
+
+ try {
+ $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
+ } catch (Duplicate $th) {
+ throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
+ }
+
+ foreach (['functions', 'sites'] as $collection) {
+ $dbForProject->updateDocuments($collection, new Document([
+ 'live' => false
+ ]));
+ }
+
+ $queueForEvents->setParam('variableId', $variable->getId());
+
+ $response->dynamic($variable, Response::MODEL_VARIABLE);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php
new file mode 100644
index 0000000000..cd11fe68c6
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/XList.php
@@ -0,0 +1,116 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/project/variables')
+ ->desc('List project variables')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.read')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'variables',
+ name: 'listVariables',
+ description: <<param('queries', [], new Variables(), '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. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('project')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->callback($this->action(...));
+ }
+
+ /**
+ * @param array $queries
+ */
+ public function action(
+ array $queries,
+ bool $includeTotal,
+ Document $project,
+ Response $response,
+ Database $dbForProject,
+ ) {
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('resourceType', ['project']);
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $variableId = $cursor->getValue();
+ $cursorDocument = $dbForProject->findOne('variables', [
+ Query::equal('$id', [$variableId]),
+ Query::equal('resourceType', ['project']),
+ ]);
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $variables = $dbForProject->find('variables', $queries);
+ $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
+ $response->dynamic(new Document([
+ 'variables' => $variables,
+ 'total' => $total,
+ ]), Response::MODEL_VARIABLE_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Module.php b/src/Appwrite/Platform/Modules/Project/Module.php
new file mode 100644
index 0000000000..ab6f445853
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Module.php
@@ -0,0 +1,14 @@
+addService('http', new Http());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php
new file mode 100644
index 0000000000..949fb2bcd9
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php
@@ -0,0 +1,29 @@
+type = Service::TYPE_HTTP;
+
+ // Hooks
+ $this->addAction(Init::getName(), new Init());
+
+ // Project
+ $this->addAction(CreateVariable::getName(), new CreateVariable());
+ $this->addAction(ListVariables::getName(), new ListVariables());
+ $this->addAction(GetVariable::getName(), new GetVariable());
+ $this->addAction(DeleteVariable::getName(), new DeleteVariable());
+ $this->addAction(UpdateVariable::getName(), new UpdateVariable());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php
index 9e32ca8276..52b94cd525 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php
@@ -85,14 +85,16 @@ class Get extends Action
$repository = $github->getRepository($owner, $repositoryName);
- $authorized = false;
- try {
- $installationRepository = $github->getInstallationRepository($repositoryName);
- if (!empty($installationRepository)) {
- $authorized = true;
+ $authorized = $github->hasAccessToAllRepositories();
+ if (!$authorized) {
+ try {
+ $installationRepository = $github->getInstallationRepository($repositoryName);
+ if (!empty($installationRepository)) {
+ $authorized = true;
+ }
+ } catch (RepositoryNotFound $e) {
+ $authorized = false;
}
- } catch (RepositoryNotFound $e) {
- $authorized = false;
}
$repository['id'] = \strval($repository['id']) ?? '';
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php
index 53713f8407..ca2c812901 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php
@@ -141,7 +141,7 @@ class XList extends Action
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
- ['items' => $repos, 'total' => $total] = $github->searchRepositories($providerInstallationId, $owner, $page, $limit, $search);
+ ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php
index af768444f2..eab6babc66 100644
--- a/src/Appwrite/Platform/Tasks/Install.php
+++ b/src/Appwrite/Platform/Tasks/Install.php
@@ -25,6 +25,7 @@ class Install extends Action
private const int HEALTH_CHECK_ATTEMPTS = 30;
private const int HEALTH_CHECK_DELAY_SECONDS = 1;
+ private const int PROC_CLOSE_TIMEOUT_SECONDS = 60;
private const string PATTERN_ENV_VAR_NAME = '/^[A-Z0-9_]+$/';
private const string PATTERN_DB_PASSWORD_VAR = '/^_APP_DB_.*_PASS$/';
@@ -169,9 +170,9 @@ class Install extends Action
}
}
- // Block database type changes on existing installations.
- // Only enforce if the existing config explicitly set _APP_DB_ADAPTER
- // (pre-1.9.0 installs never had this variable).
+ // Detect database type from existing installation.
+ // 1.9.0+ installs have _APP_DB_ADAPTER; pre-1.9.0 installs
+ // can be detected by the DB service name or _APP_DB_HOST.
$existingDatabase = null;
foreach ($compose->getServices() as $service) {
if (!$service) {
@@ -190,10 +191,15 @@ class Install extends Action
$existingDatabase = (new Env($rawEnv))->list()['_APP_DB_ADAPTER'] ?? null;
}
}
- if ($existingDatabase !== null && $existingDatabase !== $database) {
- Console::error("Cannot change database type from '{$existingDatabase}' to '{$database}'.");
- Console::error('Changing database types on an existing installation is not supported.');
- Console::exit(1);
+ if ($existingDatabase === null) {
+ $existingDatabase = $this->detectDatabaseFromCompose($compose);
+ }
+ if ($existingDatabase !== null) {
+ if ($existingDatabase !== $database) {
+ $database = $existingDatabase;
+ Console::info("Detected existing database: {$database}");
+ }
+ $vars['_APP_DB_ADAPTER']['default'] = $database;
}
}
@@ -210,7 +216,8 @@ class Install extends Action
Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT);
Console::info('Press Ctrl+C to cancel installation');
- $this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars);
+ $detectedDb = ($existingInstallation && isset($existingDatabase)) ? $existingDatabase : null;
+ $this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade, $detectedDb);
return;
}
@@ -599,7 +606,12 @@ class Install extends Action
if (!$noStart && $startIndex <= 2) {
$currentStep = InstallerServer::STEP_DOCKER_CONTAINERS;
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_IN_PROGRESS, $messages);
- $this->runDockerCompose($input, $isLocalInstall, $useExistingConfig, $isCLI);
+ $this->runDockerCompose($input, $isLocalInstall, $useExistingConfig, $isCLI, $progress, $isUpgrade);
+
+ if (!$isUpgrade) {
+ $this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_COMPLETED, $messages);
+ $this->updateProgress($progress, InstallerServer::STEP_ACCOUNT_SETUP, InstallerServer::STATUS_IN_PROGRESS, messageOverride: 'Creating Appwrite account...');
+ }
if (!$isLocalInstall) {
$this->connectInstallerToAppwriteNetwork();
@@ -607,10 +619,15 @@ class Install extends Action
$domain = $input['_APP_DOMAIN'] ?? 'localhost';
- // Wait for Appwrite API to be healthy before marking containers as ready
- $apiUrl = $this->waitForApiReady($domain, $httpPort, $isLocalInstall, $progress, InstallerServer::STEP_DOCKER_CONTAINERS);
+ $healthStep = $isUpgrade ? InstallerServer::STEP_DOCKER_CONTAINERS : InstallerServer::STEP_ACCOUNT_SETUP;
+ if (!$isUpgrade) {
+ $currentStep = InstallerServer::STEP_ACCOUNT_SETUP;
+ }
+ $apiUrl = $this->waitForApiReady($domain, $httpPort, $isLocalInstall, $progress, $healthStep);
- $this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_COMPLETED, $messages);
+ if ($isUpgrade) {
+ $this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_COMPLETED, $messages);
+ }
if (!$isUpgrade) {
$this->createInitialAdminAccount($account, $progress, $apiUrl, $domain);
@@ -658,8 +675,9 @@ class Install extends Action
messageOverride: 'Creating Appwrite account'
);
- // Create the account — tolerate "already exists" so we can still
- // create a session (common when re-running the installer).
+ // Create the account — tolerate "already exists" and "console
+ // is restricted" errors so we can still create a session
+ // (common when re-running the installer or upgrading).
$userId = null;
try {
$userId = $this->makeApiCall('/v1/account', [
@@ -669,7 +687,10 @@ class Install extends Action
'name' => $name
], false, $apiUrl, $domain);
} catch (\Throwable $e) {
- if (\stripos($e->getMessage(), 'already exists') === false) {
+ $message = $e->getMessage();
+ $accountExists = \stripos($message, 'already exists') !== false
+ || \stripos($message, 'console is restricted') !== false;
+ if (!$accountExists) {
throw $e;
}
}
@@ -732,6 +753,8 @@ class Install extends Action
$name = $account['name'] ?? 'Admin';
$email = $account['email'] ?? 'admin@selfhosted.local';
+ $hostIp = gethostbyname($domain);
+
$payload = [
'action' => $type,
'account' => 'self-hosted',
@@ -744,6 +767,11 @@ class Install extends Action
'email' => $email,
'domain' => $domain,
'database' => $database,
+ 'hostIp' => $hostIp !== $domain ? $hostIp : null,
+ 'os' => php_uname('s') . ' ' . php_uname('r'),
+ 'arch' => php_uname('m'),
+ 'cpus' => ((int) trim((string) \shell_exec('nproc'))) ?: null,
+ 'ram' => (int) round(((float) trim((string) \shell_exec('grep MemTotal /proc/meminfo | awk \'{print $2}\''))) / 1024),
]),
];
@@ -766,7 +794,7 @@ class Install extends Action
* - host.docker.internal:{port} — reaches host-published ports from inside a container
* - localhost:{port} — works when running directly on the host (local dev)
*/
- private function waitForApiReady(string $domain, string $httpPort, bool $isLocalInstall, ?callable $progress, string $step = InstallerServer::STEP_DOCKER_CONTAINERS): string
+ private function waitForApiReady(string $domain, string $httpPort, bool $isLocalInstall, ?callable $progress, string $step = InstallerServer::STEP_ACCOUNT_SETUP): string
{
$client = new Client();
$client
@@ -776,12 +804,16 @@ class Install extends Action
$healthPath = '/v1/health/version';
- // Local dev: reach Traefik via localhost on the host.
- // Docker: reach Appwrite directly via Docker internal DNS (network connect is guaranteed).
- $candidate = $isLocalInstall
- ? 'http://localhost:' . $httpPort . $healthPath
- : self::APPWRITE_API_URL . $healthPath;
- $candidates = [$candidate];
+ if ($isLocalInstall) {
+ $candidates = [
+ 'http://localhost:' . $httpPort . $healthPath,
+ ];
+ } else {
+ $candidates = [
+ self::APPWRITE_API_URL . $healthPath,
+ 'http://host.docker.internal:' . $httpPort . $healthPath,
+ ];
+ }
$lastErrors = [];
@@ -803,7 +835,7 @@ class Install extends Action
$progress(
$step,
InstallerServer::STATUS_IN_PROGRESS,
- 'Waiting for Appwrite to be ready (' . ($i + 1) . '/' . self::HEALTH_CHECK_ATTEMPTS . ')',
+ 'Waiting for Appwrite to be ready...',
[]
);
} catch (\Throwable) {
@@ -964,7 +996,7 @@ class Install extends Action
}
}
- protected function runDockerCompose(array $input, bool $isLocalInstall, bool $useExistingConfig, bool $isCLI): void
+ protected function runDockerCompose(array $input, bool $isLocalInstall, bool $useExistingConfig, bool $isCLI, ?callable $progress = null, bool $isUpgrade = false): void
{
$env = '';
if (!$useExistingConfig) {
@@ -1004,8 +1036,28 @@ class Install extends Action
$command[] = '-d';
$command[] = '--remove-orphans';
$command[] = '--renew-anon-volumes';
- $commandLine = $env . implode(' ', array_map(escapeshellarg(...), $command)) . ' 2>&1';
- \exec($commandLine, $output, $exit);
+ $commandLine = $env . implode(' ', array_map(escapeshellarg(...), $command));
+
+ if ($progress) {
+ $totalServices = $this->countComposeServices($composeFile);
+ if ($totalServices > 0) {
+ $verb = $isUpgrade ? 'Restarting' : 'Starting';
+ try {
+ $progress(
+ InstallerServer::STEP_DOCKER_CONTAINERS,
+ InstallerServer::STATUS_IN_PROGRESS,
+ "$verb Docker containers...",
+ ['containerStarted' => 0, 'containerTotal' => $totalServices]
+ );
+ } catch (\Throwable) {
+ }
+ }
+ $result = $this->execWithContainerProgress($commandLine, $totalServices, $progress, $isUpgrade);
+ $output = $result['output'];
+ $exit = $result['exit'];
+ } else {
+ \exec($commandLine . ' 2>&1', $output, $exit);
+ }
if ($exit !== 0) {
$message = trim(implode("\n", $output));
@@ -1017,6 +1069,126 @@ class Install extends Action
}
}
+ private function countComposeServices(string $composeFile): int
+ {
+ $content = @file_get_contents($composeFile);
+ if ($content === false) {
+ return 0;
+ }
+ $count = preg_match_all('/^\s*container_name:/m', $content);
+ return $count !== false ? $count : 0;
+ }
+
+ private function execWithContainerProgress(string $commandLine, int $totalServices, callable $progress, bool $isUpgrade): array
+ {
+ $verb = $isUpgrade ? 'Restarting' : 'Starting';
+ $message = "$verb Docker containers...";
+ $started = 0;
+ $output = [];
+
+ $process = proc_open(
+ $commandLine . ' 2>&1',
+ [1 => ['pipe', 'w']],
+ $pipes
+ );
+
+ if (!is_resource($process)) {
+ return ['output' => [], 'exit' => 1];
+ }
+
+ stream_set_blocking($pipes[1], false);
+ $deadline = time() + self::PROC_CLOSE_TIMEOUT_SECONDS;
+ $buffer = '';
+
+ while (time() < $deadline) {
+ $status = proc_get_status($process);
+
+ $read = [$pipes[1]];
+ $write = null;
+ $except = null;
+ $changed = @stream_select($read, $write, $except, 1);
+
+ if ($changed > 0) {
+ $chunk = fread($pipes[1], 8192);
+ if ($chunk === false || $chunk === '') {
+ if (!$status['running']) {
+ break;
+ }
+ continue;
+ }
+ $buffer .= $chunk;
+ while (($pos = strpos($buffer, "\n")) !== false) {
+ $trimmed = rtrim(substr($buffer, 0, $pos), "\r");
+ $buffer = substr($buffer, $pos + 1);
+ $output[] = $trimmed;
+
+ if (str_contains($trimmed, 'Container') && (str_contains($trimmed, 'Started') || str_contains($trimmed, 'Running'))) {
+ $started = min($started + 1, $totalServices);
+ if ($totalServices > 0) {
+ try {
+ $progress(
+ InstallerServer::STEP_DOCKER_CONTAINERS,
+ InstallerServer::STATUS_IN_PROGRESS,
+ $message,
+ ['containerStarted' => $started, 'containerTotal' => $totalServices]
+ );
+ } catch (\Throwable) {
+ }
+ }
+ }
+ }
+ }
+
+ if (!$status['running'] && ($changed === 0 || feof($pipes[1]))) {
+ break;
+ }
+ }
+
+ if ($buffer !== '') {
+ $output[] = rtrim($buffer, "\r\n");
+ }
+
+ fclose($pipes[1]);
+
+ $exit = $this->procCloseWithTimeout($process, self::PROC_CLOSE_TIMEOUT_SECONDS);
+
+ return ['output' => $output, 'exit' => $exit];
+ }
+
+ /**
+ * Wait up to $timeoutSeconds for a process to exit, then kill it.
+ *
+ * proc_close() blocks indefinitely which can hang the installer if
+ * docker compose refuses to exit after all containers are running.
+ *
+ * @param resource $process A process resource from proc_open()
+ */
+ private function procCloseWithTimeout($process, int $timeoutSeconds): int
+ {
+ $deadline = time() + $timeoutSeconds;
+
+ while (time() < $deadline) {
+ $status = proc_get_status($process);
+ if (!$status['running']) {
+ $exitCode = $status['exitcode'];
+ $closeCode = proc_close($process);
+ return $exitCode !== -1 ? $exitCode : $closeCode;
+ }
+ usleep(250_000);
+ }
+
+ proc_terminate($process, SIGTERM);
+ usleep(500_000);
+
+ if (proc_get_status($process)['running']) {
+ proc_terminate($process, SIGKILL);
+ }
+
+ proc_close($process);
+
+ return 124;
+ }
+
protected function isLocalInstall(): bool
{
if ($this->isLocalInstall === null) {
@@ -1089,6 +1261,34 @@ class Install extends Action
$this->hostPath = $this->getInstallerHostPath();
}
+ /**
+ * Detect the database adapter from a pre-1.9.0 compose file by
+ * checking which DB service exists or reading _APP_DB_HOST.
+ */
+ private function detectDatabaseFromCompose(Compose $compose): ?string
+ {
+ $serviceNames = array_keys($compose->getServices());
+ $dbServices = ['mariadb', 'mongodb', 'postgresql'];
+ foreach ($dbServices as $db) {
+ if (in_array($db, $serviceNames, true)) {
+ return $db;
+ }
+ }
+
+ foreach ($compose->getServices() as $service) {
+ if (!$service) {
+ continue;
+ }
+ $env = $service->getEnvironment()->list();
+ $host = $env['_APP_DB_HOST'] ?? null;
+ if ($host !== null && in_array($host, $dbServices, true)) {
+ return $host;
+ }
+ }
+
+ return null;
+ }
+
protected function readExistingCompose(): string
{
$composeFile = $this->path . '/' . $this->getComposeFileName();
diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php
index 3065b2377f..716969e67a 100644
--- a/src/Appwrite/Platform/Workers/Deletes.php
+++ b/src/Appwrite/Platform/Workers/Deletes.php
@@ -54,6 +54,7 @@ class Deletes extends Action
->inject('project')
->inject('dbForPlatform')
->inject('getProjectDB')
+ ->inject('getDatabasesDB')
->inject('getLogsDB')
->inject('deviceForFiles')
->inject('deviceForFunctions')
@@ -80,6 +81,7 @@ class Deletes extends Action
Document $project,
Database $dbForPlatform,
callable $getProjectDB,
+ callable $getDatabasesDB,
callable $getLogsDB,
Device $deviceForFiles,
Device $deviceForFunctions,
@@ -115,7 +117,7 @@ class Deletes extends Action
case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) {
case DELETE_TYPE_PROJECTS:
- $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
+ $this->deleteProject($dbForPlatform, $getProjectDB, $getDatabasesDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break;
case DELETE_TYPE_SITES:
$this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
@@ -150,7 +152,7 @@ class Deletes extends Action
}
break;
case DELETE_TYPE_TEAM_PROJECTS:
- $this->deleteProjectsByTeam($dbForPlatform, $getProjectDB, $certificates, $document);
+ $this->deleteProjectsByTeam($dbForPlatform, $getProjectDB, $getDatabasesDB, $certificates, $document);
break;
case DELETE_TYPE_EXECUTIONS:
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention);
@@ -220,6 +222,50 @@ class Deletes extends Action
}
}
+ private function cleanDatabase(
+ Document $databaseDoc,
+ callable $executionActionPerDatabase,
+ bool $projectTables,
+ array $projectCollectionIds
+ ): void {
+ $executionActionPerDatabase(
+ $databaseDoc,
+ fn (Database $dbForDatabases) => $this->cleanDatabaseCollections(
+ $dbForDatabases,
+ $projectTables,
+ $projectCollectionIds
+ )
+ );
+ }
+
+ private function cleanDatabaseCollections(
+ Database $dbForDatabases,
+ bool $projectTables,
+ array $projectCollectionIds
+ ): void {
+ $dbForDatabases->foreach(
+ Database::METADATA,
+ function (Document $collection) use ($dbForDatabases, $projectTables, $projectCollectionIds) {
+ $collectionId = $collection->getId();
+
+ try {
+ if ($projectTables || !\in_array($collectionId, $projectCollectionIds, true)) {
+ $dbForDatabases->deleteCollection($collectionId);
+ return;
+ }
+
+ $this->deleteByGroup(
+ $collectionId,
+ [Query::orderAsc()],
+ database: $dbForDatabases
+ );
+ } catch (Throwable $e) {
+ Console::error('Error deleting ' . $collectionId . ' ' . $e->getMessage());
+ }
+ }
+ );
+ }
+
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
@@ -547,7 +593,7 @@ class Deletes extends Action
* @throws Structure
* @throws Exception
*/
- protected function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
+ protected function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, CertificatesAdapter $certificates, Document $document): void
{
$projects = $dbForPlatform->find('projects', [
@@ -562,7 +608,7 @@ class Deletes extends Action
$deviceForBuilds = getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
$deviceForCache = getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
- $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $project);
+ $this->deleteProject($dbForPlatform, $getProjectDB, $getDatabasesDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $project);
$dbForPlatform->deleteDocument('projects', $project->getId());
}
}
@@ -580,7 +626,7 @@ class Deletes extends Action
* @throws Authorization
* @throws DatabaseException
*/
- protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
+ protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
{
$projectInternalId = $document->getSequence();
$projectId = $document->getId();
@@ -617,23 +663,44 @@ class Deletes extends Action
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
- $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) {
- try {
- if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) {
- $dbForProject->deleteCollection($collection->getId());
- } else {
- $this->deleteByGroup(
- $collection->getId(),
- [
- Query::orderAsc()
- ],
- database: $dbForProject
- );
- }
- } catch (Throwable $e) {
- Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage());
+ $allDatabases = [
+ new Document([
+ 'database' => $document->getAttribute('database')
+ ]),
+ ...$dbForProject->find('databases', [
+ Query::equal('type', [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]),
+ Query::limit(5000),
+ ]),
+ ];
+ $databasesToClean = [];
+
+ foreach ($allDatabases as $db) {
+ $key = $db->getAttribute('database');
+
+ if ($key) {
+ $databasesToClean[$key] ??= $db;
}
- });
+ }
+
+ $databasesToClean = array_values($databasesToClean);
+
+ $executionActionPerDatabase = function (Document $databaseDoc, $callback) use ($getDatabasesDB, $document) {
+ /**
+ * @var Database $dbForDatabases
+ */
+ $dbForDatabases = $getDatabasesDB($databaseDoc, $document);
+ $callback($dbForDatabases);
+ };
+
+ batch(array_map(
+ fn ($databaseDoc) => fn () => $this->cleanDatabase(
+ $databaseDoc,
+ $executionActionPerDatabase,
+ $projectTables,
+ $projectCollectionIds
+ ),
+ $databasesToClean
+ ));
// Delete Platforms
$this->deleteByGroup('platforms', [
@@ -688,7 +755,15 @@ class Deletes extends Action
// Delete metadata table
if ($projectTables) {
- $dbForProject->deleteCollection(Database::METADATA);
+ batch(array_map(
+ fn ($databaseDoc) => fn () =>
+ $executionActionPerDatabase(
+ $databaseDoc,
+ fn (Database $dbForDatabases) =>
+ $dbForDatabases->deleteCollection(Database::METADATA)
+ ),
+ $databasesToClean
+ ));
} elseif ($sharedTablesV1) {
$this->deleteByGroup(
Database::METADATA,
diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php
index 25d5bfa027..d96a25351f 100644
--- a/src/Appwrite/Platform/Workers/Migrations.php
+++ b/src/Appwrite/Platform/Workers/Migrations.php
@@ -52,6 +52,18 @@ class Migrations extends Action
protected ?Device $deviceForMigrations;
protected ?Device $deviceForFiles;
protected ?Document $project;
+
+ protected Document $sourceProject;
+
+ /**
+ * @var callable
+ */
+ protected mixed $getDatabasesDB;
+
+ /**
+ * @var callable(Document $databaseDSN): Database
+ */
+ protected mixed $getProjectDB;
protected array $plan = [];
/**
@@ -81,6 +93,8 @@ class Migrations extends Action
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
+ ->inject('getDatabasesDB')
+ ->inject('getProjectDB')
->inject('logError')
->inject('queueForRealtime')
->inject('deviceForMigrations')
@@ -101,6 +115,8 @@ class Migrations extends Action
Document $project,
Database $dbForProject,
Database $dbForPlatform,
+ callable $getDatabasesDB,
+ callable $getProjectDB,
callable $logError,
Realtime $queueForRealtime,
Device $deviceForMigrations,
@@ -112,6 +128,9 @@ class Migrations extends Action
Authorization $authorization,
): void {
$payload = $message->getPayload() ?? [];
+ $this->getDatabasesDB = $getDatabasesDB;
+ $this->getProjectDB = $getProjectDB;
+
$this->deviceForMigrations = $deviceForMigrations;
$this->deviceForFiles = $deviceForFiles;
$this->plan = $plan;
@@ -180,13 +199,16 @@ class Migrations extends Action
$resourceId = $migration->getAttribute('resourceId');
$credentials = $migration->getAttribute('credentials');
$migrationOptions = $migration->getAttribute('options');
- $dataSource = SourceAppwrite::SOURCE_API;
- $database = null;
+ /** @var Database|null $projectDB */
+ $projectDB = null;
+ if ($credentials['projectId']) {
+ $this->sourceProject = $this->dbForPlatform->getDocument('projects', $credentials['projectId']);
+ $projectDB = call_user_func($this->getProjectDB, $this->sourceProject);
+ }
+ $getDatabasesDB = fn (Document $database): Database =>
+ $this->getDatabasesDBForProject($database);
$queries = [];
-
if ($source === SourceAppwrite::getName() && $destination === DestinationCSV::getName()) {
- $dataSource = SourceAppwrite::SOURCE_DATABASE;
- $database = $this->dbForProject;
$queries = Query::parseQueries($migrationOptions['queries']);
}
@@ -216,15 +238,17 @@ class Migrations extends Action
$credentials['projectId'],
$credentials['endpoint'],
$credentials['apiKey'],
- $dataSource,
- $database,
- $queries,
+ $getDatabasesDB,
+ SourceAppwrite::SOURCE_DATABASE,
+ $projectDB,
+ $queries
),
CSV::getName() => new CSV(
$resourceId,
$migrationOptions['path'],
$this->deviceForMigrations,
- $this->dbForProject
+ $this->dbForProject,
+ $getDatabasesDB
),
default => throw new \Exception('Invalid source type'),
};
@@ -250,6 +274,7 @@ class Migrations extends Action
$credentials['destinationEndpoint'],
$credentials['destinationApiKey'],
$this->dbForProject,
+ $this->getDatabasesDB,
Config::getParam('collections', [])['databases']['collections'],
),
DestinationCSV::getName() => new DestinationCSV(
@@ -303,6 +328,10 @@ class Migrations extends Action
'disabledMetrics' => [
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_WRITES,
+ METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
+ METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
+ METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
+ METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
METRIC_NETWORK_REQUESTS,
METRIC_NETWORK_INBOUND,
METRIC_NETWORK_OUTBOUND,
@@ -333,7 +362,9 @@ class Migrations extends Action
'targets.read',
'targets.write',
'webhooks.read',
- 'webhooks.write'
+ 'webhooks.write',
+ 'project.read',
+ 'project.write'
]
]);
@@ -518,11 +549,9 @@ class Migrations extends Action
}
$destination?->success();
$source?->success();
-
- // TODO: Move to CSV hook
- if ($migration->getAttribute('destination') === DestinationCSV::getName()) {
- $this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
- }
+ }
+ if ($migration->getAttribute('destination') === DestinationCSV::getName()) {
+ $this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
}
} finally {
$source?->cleanup();
@@ -535,6 +564,14 @@ class Migrations extends Action
}
}
+ protected function getDatabasesDBForProject(Document $database)
+ {
+ if ($this->sourceProject) {
+ return ($this->getDatabasesDB)($database, $this->sourceProject);
+ }
+ return ($this->getDatabasesDB)($database);
+ }
+
/**
* Handle actions to be performed when a CSV export migration is successfully completed
*
diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php
index e464455470..0e7a9bb0a7 100644
--- a/src/Appwrite/Platform/Workers/StatsResources.php
+++ b/src/Appwrite/Platform/Workers/StatsResources.php
@@ -47,6 +47,7 @@ class StatsResources extends Action
->inject('project')
->inject('getProjectDB')
->inject('getLogsDB')
+ ->inject('getDatabasesDB')
->inject('dbForPlatform')
->inject('logError')
->callback($this->action(...));
@@ -56,11 +57,13 @@ class StatsResources extends Action
* @param Message $message
* @param Document $project
* @param callable $getProjectDB
+ * @param callable $getLogsDB
+ * @param callable $getDatabasesDB
* @return void
* @throws \Utopia\Database\Exception
* @throws Exception
*/
- public function action(Message $message, Document $project, callable $getProjectDB, callable $getLogsDB, Database $dbForPlatform, callable $logError): void
+ public function action(Message $message, Document $project, callable $getProjectDB, callable $getLogsDB, callable $getDatabasesDB, Database $dbForPlatform, callable $logError): void
{
$this->logError = $logError;
@@ -76,10 +79,10 @@ class StatsResources extends Action
// Reset documents for each job
$this->documents = [];
- $this->countForProject($dbForPlatform, $getLogsDB, $getProjectDB, $project);
+ $this->countForProject($dbForPlatform, $getLogsDB, $getProjectDB, $getDatabasesDB, $project);
}
- protected function countForProject(Database $dbForPlatform, callable $getLogsDB, callable $getProjectDB, Document $project): void
+ protected function countForProject(Database $dbForPlatform, callable $getLogsDB, callable $getProjectDB, callable $getDatabasesDB, Document $project): void
{
/** @var \Utopia\Database\Database $dbForLogs */
$dbForLogs = call_user_func($getLogsDB, $project);
@@ -107,7 +110,9 @@ class StatsResources extends Action
]);
- $databases = $dbForProject->count('databases');
+ $databases = $dbForProject->count('databases', [Query::equal('type', [DATABASE_TYPE_LEGACY, DATABASE_TYPE_TABLESDB])]);
+ $documentsdb = $dbForProject->count('databases', [Query::equal('type', [DATABASE_TYPE_DOCUMENTSDB])]);
+ $vectorsdb = $dbForProject->count('databases', [Query::equal('type', [DATABASE_TYPE_VECTORSDB])]);
$buckets = $dbForProject->count('buckets');
$users = $dbForProject->count('users');
@@ -142,6 +147,8 @@ class StatsResources extends Action
$metrics = [
METRIC_DATABASES => $databases,
+ METRIC_DATABASES_DOCUMENTSDB => $documentsdb,
+ METRIC_DATABASES_VECTORSDB => $vectorsdb,
METRIC_BUCKETS => $buckets,
METRIC_USERS => $users,
METRIC_FUNCTIONS => $functions,
@@ -179,7 +186,7 @@ class StatsResources extends Action
}
try {
- $dbForProject->skipFilters(fn () => $this->countForDatabase($dbForProject, $region), ['subQueryAttributes', 'subQueryIndexes']);
+ $dbForProject->skipFilters(fn () => $this->countForDatabase($dbForProject, $getDatabasesDB, $region), ['subQueryAttributes', 'subQueryIndexes']);
} catch (Throwable $th) {
call_user_func_array($this->logError, [$th, "StatsResources", "count_for_database_{$project->getId()}"]);
}
@@ -255,51 +262,101 @@ class StatsResources extends Action
$this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalImageTransformations);
}
- protected function countForDatabase(Database $dbForProject, string $region)
+ protected function countForDatabase(Database $dbForProject, callable $getDatabasesDB, string $region)
{
$totalCollections = 0;
$totalDocuments = 0;
-
$totalDatabaseStorage = 0;
- $this->foreachDocument($dbForProject, 'databases', [], function ($database) use ($dbForProject, $region, &$totalCollections, &$totalDocuments, &$totalDatabaseStorage) {
+ // documentsdb
+ $totalCollectionsDocumentsdb = 0;
+ $totalDocumentsDocumentsdb = 0;
+ $totalDatabaseStorageDocumentsdb = 0;
+
+ // vectorsdb
+ $totalCollectionsVectordb = 0;
+ $totalDocumentsVectordb = 0;
+ $totalDatabaseStorageVectordb = 0;
+
+
+ $this->foreachDocument($dbForProject, 'databases', [], function ($database) use ($dbForProject, $getDatabasesDB, $region, &$totalCollections, &$totalDocuments, &$totalDatabaseStorage, &$totalCollectionsDocumentsdb, &$totalDocumentsDocumentsdb, &$totalDatabaseStorageDocumentsdb, &$totalCollectionsVectordb, &$totalDocumentsVectordb, &$totalDatabaseStorageVectordb) {
+ $dbForDatabases = $getDatabasesDB($database);
$collections = $dbForProject->count('database_' . $database->getSequence());
- $metric = str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_COLLECTIONS);
+ $databaseType = $database->getAttribute('type');
+ $collectionsMetric = METRIC_DATABASE_ID_COLLECTIONS;
+ if (!empty($databaseType) && $databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
+ $collectionsMetric = $databaseType . '.' . $collectionsMetric;
+ }
+ $metric = str_replace('{databaseInternalId}', $database->getSequence(), $collectionsMetric);
$this->createStatsDocuments($region, $metric, $collections);
- [$documents, $storage] = $this->countForCollections($dbForProject, $database, $region);
+ [$documents, $storage] = $this->countForCollections($dbForProject, $dbForDatabases, $database, $region);
- $totalDatabaseStorage += $storage;
- $totalDocuments += $documents;
- $totalCollections += $collections;
+ switch ($database->getAttribute('type')) {
+ case DATABASE_TYPE_DOCUMENTSDB:
+ $totalDatabaseStorageDocumentsdb += $storage;
+ $totalDocumentsDocumentsdb += $documents;
+ $totalCollectionsDocumentsdb += $collections;
+ break;
+ case DATABASE_TYPE_VECTORSDB:
+ $totalDatabaseStorageVectordb += $storage;
+ $totalDocumentsVectordb += $documents;
+ $totalCollectionsVectordb += $collections;
+ break;
+ default:
+ $totalDatabaseStorage += $storage;
+ $totalDocuments += $documents;
+ $totalCollections += $collections;
+ }
});
$this->createStatsDocuments($region, METRIC_COLLECTIONS, $totalCollections);
$this->createStatsDocuments($region, METRIC_DOCUMENTS, $totalDocuments);
$this->createStatsDocuments($region, METRIC_DATABASES_STORAGE, $totalDatabaseStorage);
+
+ $this->createStatsDocuments($region, METRIC_COLLECTIONS_DOCUMENTSDB, $totalCollectionsDocumentsdb);
+ $this->createStatsDocuments($region, METRIC_DOCUMENTS_DOCUMENTSDB, $totalDocumentsDocumentsdb);
+ $this->createStatsDocuments($region, METRIC_DATABASES_STORAGE_DOCUMENTSDB, $totalDatabaseStorageDocumentsdb);
+
+ $this->createStatsDocuments($region, METRIC_COLLECTIONS_VECTORSDB, $totalCollectionsVectordb);
+ $this->createStatsDocuments($region, METRIC_DOCUMENTS_VECTORSDB, $totalDocumentsVectordb);
+ $this->createStatsDocuments($region, METRIC_DATABASES_STORAGE_VECTORSDB, $totalDatabaseStorageVectordb);
}
- protected function countForCollections(Database $dbForProject, Document $database, string $region): array
+ protected function countForCollections(Database $dbForProject, Database $dbForDatabases, Document $database, string $region): array
{
$databaseDocuments = 0;
$databaseStorage = 0;
- $this->foreachDocument($dbForProject, 'database_' . $database->getSequence(), [], function ($collection) use ($dbForProject, $database, $region, &$databaseStorage, &$databaseDocuments) {
- $documents = $dbForProject->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
- $metric = str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collection->getSequence()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS);
+ $databaseType = $database->getAttribute('type');
+ $databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
+ $databaseIdCollectionIdStorageMetric = METRIC_DATABASE_ID_COLLECTION_ID_STORAGE;
+ $databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS;
+ $databaseIdStorageMetric = METRIC_DATABASE_ID_STORAGE;
+
+ if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
+ $databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric;
+ $databaseIdCollectionIdStorageMetric = $databaseType . '.' . $databaseIdCollectionIdStorageMetric;
+ $databaseIdDocumentsMetric = $databaseType . '.' . $databaseIdDocumentsMetric;
+ $databaseIdStorageMetric = $databaseType . '.' . $databaseIdStorageMetric;
+ }
+
+ $this->foreachDocument($dbForProject, 'database_' . $database->getSequence(), [], function ($collection) use ($dbForDatabases, $database, $region, &$databaseStorage, &$databaseDocuments, $databaseIdCollectionIdDocumentsMetric, $databaseIdCollectionIdStorageMetric) {
+ $documents = $dbForDatabases->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
+ $metric = str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collection->getSequence()], $databaseIdCollectionIdDocumentsMetric);
$this->createStatsDocuments($region, $metric, $documents);
$databaseDocuments += $documents;
- $collectionStorage = $dbForProject->getSizeOfCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
- $metric = str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collection->getSequence()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE);
+ $collectionStorage = $dbForDatabases->getSizeOfCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
+ $metric = str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collection->getSequence()], $databaseIdCollectionIdStorageMetric);
$this->createStatsDocuments($region, $metric, $collectionStorage);
$databaseStorage += $collectionStorage;
});
- $metric = str_replace(['{databaseInternalId}'], [$database->getSequence()], METRIC_DATABASE_ID_DOCUMENTS);
+ $metric = str_replace(['{databaseInternalId}'], [$database->getSequence()], $databaseIdDocumentsMetric);
$this->createStatsDocuments($region, $metric, $databaseDocuments);
- $metric = str_replace(['{databaseInternalId}'], [$database->getSequence()], METRIC_DATABASE_ID_STORAGE);
+ $metric = str_replace(['{databaseInternalId}'], [$database->getSequence()], $databaseIdStorageMetric);
$this->createStatsDocuments($region, $metric, $databaseStorage);
return [$databaseDocuments, $databaseStorage];
diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php
index 76be33d06b..1e0a2eabba 100644
--- a/src/Appwrite/Platform/Workers/StatsUsage.php
+++ b/src/Appwrite/Platform/Workers/StatsUsage.php
@@ -47,6 +47,8 @@ class StatsUsage extends Action
*/
protected array $skipBaseMetrics = [
METRIC_DATABASES => true,
+ METRIC_DATABASES_DOCUMENTSDB => true,
+ METRIC_DATABASES_VECTORSDB => true,
METRIC_BUCKETS => true,
METRIC_USERS => true,
METRIC_FUNCTIONS => true,
@@ -66,7 +68,13 @@ class StatsUsage extends Action
METRIC_BUILDS => true,
METRIC_COLLECTIONS => true,
METRIC_DOCUMENTS => true,
+ METRIC_COLLECTIONS_DOCUMENTSDB => true,
+ METRIC_DOCUMENTS_DOCUMENTSDB => true,
+ METRIC_COLLECTIONS_VECTORSDB => true,
+ METRIC_DOCUMENTS_VECTORSDB => true,
METRIC_DATABASES_STORAGE => true,
+ METRIC_DATABASES_STORAGE_DOCUMENTSDB => true,
+ METRIC_DATABASES_STORAGE_VECTORSDB => true,
];
/**
@@ -85,6 +93,12 @@ class StatsUsage extends Action
'.databases.storage'
];
+ public const DATABASE_PREFIXES = [
+ DATABASE_TYPE_LEGACY,
+ DATABASE_TYPE_TABLESDB,
+ DATABASE_TYPE_DOCUMENTSDB,
+ ];
+
/**
* @var callable(): Database
*/
@@ -146,6 +160,11 @@ class StatsUsage extends Action
$aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
$project = new Document($payload['project'] ?? []);
$projectId = $project->getSequence();
+
+ // Get database type from context
+ $databaseContext = $payload['context']['database'] ?? null;
+ $databaseType = $databaseContext ? (new Document($databaseContext))->getAttribute('type', '') : '';
+
foreach ($payload['reduce'] ?? [] as $document) {
if (empty($document)) {
continue;
@@ -155,7 +174,8 @@ class StatsUsage extends Action
project: $project,
document: new Document($document),
metrics: $payload['metrics'],
- getProjectDB: $getProjectDB
+ getProjectDB: $getProjectDB,
+ databaseType: $databaseType
);
}
@@ -193,9 +213,10 @@ class StatsUsage extends Action
* @param Document $document
* @param array $metrics
* @param callable(): Database $getProjectDB
+ * @param string $databaseType Database type from context
* @return void
*/
- protected function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB): void
+ protected function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB, string $databaseType = ''): void
{
$dbForProject = $getProjectDB($project);
@@ -211,38 +232,48 @@ class StatsUsage extends Action
}
break;
case $document->getCollection() === 'databases': // databases
- $collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), METRIC_DATABASE_ID_COLLECTIONS)));
- $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), METRIC_DATABASE_ID_DOCUMENTS)));
+ $databaseCollectionsMetric = implode('.', array_filter([$databaseType,METRIC_COLLECTIONS]));
+ $databaseDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DOCUMENTS]));
+
+ $databaseIdCollectionsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_COLLECTIONS]));
+ $databaseIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_DOCUMENTS]));
+
+ $collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), $databaseIdCollectionsMetric)));
+ $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), $databaseIdDocumentsMetric)));
if (!empty($collections['value'])) {
$metrics[] = [
- 'key' => METRIC_COLLECTIONS,
+ 'key' => $databaseCollectionsMetric,
'value' => ($collections['value'] * -1),
];
}
if (!empty($documents['value'])) {
$metrics[] = [
- 'key' => METRIC_DOCUMENTS,
+ 'key' => $databaseDocumentsMetric,
'value' => ($documents['value'] * -1),
];
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
+ $databaseDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DOCUMENTS]));
+ $databaseIdCollectionIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS]));
+ $databaseIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_DOCUMENTS]));
+
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(
['{databaseInternalId}', '{collectionInternalId}'],
[$databaseInternalId, $document->getSequence()],
- METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS
+ $databaseIdCollectionIdDocumentsMetric
)));
if (!empty($documents['value'])) {
$metrics[] = [
- 'key' => METRIC_DOCUMENTS,
+ 'key' => $databaseDocumentsMetric,
'value' => ($documents['value'] * -1),
];
$metrics[] = [
- 'key' => str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS),
+ 'key' => str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric),
'value' => ($documents['value'] * -1),
];
}
@@ -473,8 +504,11 @@ class StatsUsage extends Action
if (array_key_exists($stat->getAttribute('metric'), $this->skipBaseMetrics)) {
return;
}
+
foreach ($this->skipParentIdMetrics as $skipMetric) {
- if (str_ends_with($stat->getAttribute('metric'), $skipMetric)) {
+ $metricParts = explode('.', $stat->getAttribute('metric'));
+ $metric = implode('.', in_array($metricParts[0], self::DATABASE_PREFIXES) ? array_slice($metricParts, 1) : $metricParts);
+ if (str_ends_with($metric, $skipMetric)) {
return;
}
}
diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php
index bd2f063073..04ecafa8fc 100644
--- a/src/Appwrite/SDK/Specification/Format.php
+++ b/src/Appwrite/SDK/Specification/Format.php
@@ -309,7 +309,7 @@ abstract class Format
case 'createIndex':
switch ($param) {
case 'type':
- return 'IndexType';
+ return 'DatabasesIndexType';
case 'orders':
return 'OrderBy';
}
@@ -342,7 +342,45 @@ abstract class Format
case 'createIndex':
switch ($param) {
case 'type':
- return 'IndexType';
+ return 'TablesDBIndexType';
+ case 'orders':
+ return 'OrderBy';
+ }
+ }
+ break;
+ case 'documentsDB':
+ switch ($method) {
+ case 'getUsage':
+ case 'listUsage':
+ case 'getCollectionUsage':
+ switch ($param) {
+ case 'range':
+ return 'UsageRange';
+ }
+ break;
+ case 'createIndex':
+ switch ($param) {
+ case 'type':
+ return 'DocumentsDBIndexType';
+ case 'orders':
+ return 'OrderBy';
+ }
+ }
+ break;
+ case 'vectorsDB':
+ switch ($method) {
+ case 'getUsage':
+ case 'listUsage':
+ case 'getCollectionUsage':
+ switch ($param) {
+ case 'range':
+ return 'UsageRange';
+ }
+ break;
+ case 'createIndex':
+ switch ($param) {
+ case 'type':
+ return 'VectorsDBIndexType';
case 'orders':
return 'OrderBy';
}
@@ -630,6 +668,8 @@ abstract class Format
}
break;
case 'databases':
+ case 'documentsDB':
+ case 'vectorsDB':
switch ($method) {
case 'getUsage':
case 'listUsage':
diff --git a/src/Appwrite/Utopia/Database/Validator/Attributes.php b/src/Appwrite/Utopia/Database/Validator/Attributes.php
index aac5ec2f37..f8bdd01103 100644
--- a/src/Appwrite/Utopia/Database/Validator/Attributes.php
+++ b/src/Appwrite/Utopia/Database/Validator/Attributes.php
@@ -45,10 +45,12 @@ class Attributes extends Validator
/**
* @param int $maxAttributes Maximum number of attributes allowed
* @param bool $supportForSpatialAttributes Whether DB supports spatial attributes
+ * @param bool $supportForAttributes Whether DB supports attributes or not
*/
public function __construct(
int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE,
protected bool $supportForSpatialAttributes = true,
+ protected bool $supportForAttributes = true
) {
$this->maxAttributes = $maxAttributes;
}
@@ -78,6 +80,11 @@ class Attributes extends Validator
return false;
}
+ if (\count($value) && !$this->supportForAttributes) {
+ $this->message = 'Attributes are not supported by the current database';
+ return false;
+ }
+
if (\count($value) > $this->maxAttributes) {
$this->message = 'Maximum of ' . $this->maxAttributes . ' attributes allowed';
return false;
diff --git a/src/Appwrite/Utopia/Database/Validator/Operation.php b/src/Appwrite/Utopia/Database/Validator/Operation.php
index e6884ac677..6d50611708 100644
--- a/src/Appwrite/Utopia/Database/Validator/Operation.php
+++ b/src/Appwrite/Utopia/Database/Validator/Operation.php
@@ -59,6 +59,7 @@ class Operation extends Validator
{
switch ($this->type) {
case 'legacy':
+ case 'documentsdb':
$this->collectionIdName = 'collectionId';
$this->documentIdName = 'documentId';
break;
diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Base.php b/src/Appwrite/Utopia/Database/Validator/Queries/Base.php
index 02f2a57c5b..9d9bbde00b 100644
--- a/src/Appwrite/Utopia/Database/Validator/Queries/Base.php
+++ b/src/Appwrite/Utopia/Database/Validator/Queries/Base.php
@@ -87,8 +87,6 @@ class Base extends Queries
$allAttributes[] = $attribute;
}
-
-
$validators = [
new Limit(),
new Offset(),
diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Variables.php b/src/Appwrite/Utopia/Database/Validator/Queries/Variables.php
index 5d7a5e5cee..222f571281 100644
--- a/src/Appwrite/Utopia/Database/Validator/Queries/Variables.php
+++ b/src/Appwrite/Utopia/Database/Validator/Queries/Variables.php
@@ -7,7 +7,8 @@ class Variables extends Base
public const ALLOWED_ATTRIBUTES = [
'key',
'resourceType',
- 'resourceId'
+ 'resourceId',
+ 'secret',
];
/**
diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php
index 74e1fcfaff..1fd6ba9dc4 100644
--- a/src/Appwrite/Utopia/Request/Filters/V21.php
+++ b/src/Appwrite/Utopia/Request/Filters/V21.php
@@ -2,6 +2,7 @@
namespace Appwrite\Utopia\Request\Filters;
+use Appwrite\Query;
use Appwrite\Utopia\Request\Filter;
class V21 extends Filter
@@ -13,6 +14,12 @@ class V21 extends Filter
case 'webhooks.create':
$content = $this->fillWebhookid($content);
break;
+ case 'project.createVariable':
+ $content = $this->fillVariableId($content);
+ break;
+ case 'project.listVariables':
+ $content = $this->preserveVariablesQueries($content);
+ break;
case 'functions.createTemplateDeployment':
case 'sites.createTemplateDeployment':
$content = $this->convertVersionToTypeAndReference($content);
@@ -57,4 +64,19 @@ class V21 extends Filter
$content['webhookId'] = $content['webhookId'] ?? 'unique()';
return $content;
}
+
+ protected function fillVariableId(array $content): array
+ {
+ $content['variableId'] = $content['variableId'] ?? 'unique()';
+ return $content;
+ }
+
+ protected function preserveVariablesQueries(array $content): array
+ {
+ $content['queries'] = $content['queries'] ?? [
+ Query::limit(APP_LIMIT_SUBQUERY)
+ ];
+
+ return $content;
+ }
}
diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php
index 682c645047..c2fc520da3 100644
--- a/src/Appwrite/Utopia/Response.php
+++ b/src/Appwrite/Utopia/Response.php
@@ -32,6 +32,10 @@ class Response extends SwooleResponse
public const MODEL_BASE_LIST = 'baseList';
public const MODEL_USAGE_DATABASES = 'usageDatabases';
public const MODEL_USAGE_DATABASE = 'usageDatabase';
+ public const MODEL_USAGE_DOCUMENTSDBS = 'usageDocumentsDBs';
+ public const MODEL_USAGE_DOCUMENTSDB = 'usageDocumentsDB';
+ public const MODEL_USAGE_VECTORSDBS = 'usageVectorsDBs';
+ public const MODEL_USAGE_VECTORSDB = 'usageVectorsDB';
public const MODEL_USAGE_TABLE = 'usageTable';
public const MODEL_USAGE_COLLECTION = 'usageCollection';
public const MODEL_USAGE_USERS = 'usageUsers';
@@ -48,6 +52,10 @@ class Response extends SwooleResponse
public const MODEL_DATABASE_LIST = 'databaseList';
public const MODEL_COLLECTION = 'collection';
public const MODEL_COLLECTION_LIST = 'collectionList';
+ public const MODEL_VECTORSDB_COLLECTION = 'vectorsdbCollection';
+ public const MODEL_VECTORSDB_COLLECTION_LIST = 'vectorsdbCollectionList';
+ public const MODEL_EMBEDDING = 'embedding';
+ public const MODEL_EMBEDDING_LIST = 'embeddingList';
public const MODEL_TABLE = 'table';
public const MODEL_TABLE_LIST = 'tableList';
public const MODEL_INDEX = 'index';
@@ -79,6 +87,8 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_TEXT = 'attributeText';
public const MODEL_ATTRIBUTE_MEDIUMTEXT = 'attributeMediumtext';
public const MODEL_ATTRIBUTE_LONGTEXT = 'attributeLongtext';
+ public const MODEL_ATTRIBUTE_OBJECT = 'attributeObject';
+ public const MODEL_ATTRIBUTE_VECTOR = 'attributeVector';
// Database Columns
public const MODEL_COLUMN = 'column';
diff --git a/src/Appwrite/Utopia/Response/Model/AttributeObject.php b/src/Appwrite/Utopia/Response/Model/AttributeObject.php
new file mode 100644
index 0000000000..542f7f744c
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/AttributeObject.php
@@ -0,0 +1,27 @@
+ 'object',
+ ];
+
+ public function getName(): string
+ {
+ return 'AttributeObject';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_ATTRIBUTE_OBJECT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/AttributeVector.php b/src/Appwrite/Utopia/Response/Model/AttributeVector.php
new file mode 100644
index 0000000000..4b58b979ee
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/AttributeVector.php
@@ -0,0 +1,35 @@
+addRule('size', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Vector dimensions.',
+ 'default' => 0,
+ 'example' => 1536,
+ ]);
+ }
+
+ public array $conditions = [
+ 'type' => 'vector',
+ ];
+
+ public function getName(): string
+ {
+ return 'AttributeVector';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_ATTRIBUTE_VECTOR;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Database.php b/src/Appwrite/Utopia/Response/Model/Database.php
index 59f32b3162..df9ad2e1f5 100644
--- a/src/Appwrite/Utopia/Response/Model/Database.php
+++ b/src/Appwrite/Utopia/Response/Model/Database.php
@@ -45,9 +45,8 @@ class Database extends Model
'description' => 'Database type.',
'default' => 'legacy',
'example' => 'legacy',
- 'enum' => ['legacy', 'tablesdb'],
- ])
- ;
+ 'enum' => ['legacy', 'tablesdb', 'documentsdb', 'vectorsdb'],
+ ]);
}
/**
diff --git a/src/Appwrite/Utopia/Response/Model/Embedding.php b/src/Appwrite/Utopia/Response/Model/Embedding.php
new file mode 100644
index 0000000000..9fce913723
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Embedding.php
@@ -0,0 +1,47 @@
+addRule('model', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Embedding model used to generate embeddings.',
+ 'example' => 'embeddinggemma'
+ ])
+ ->addRule('dimension', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Number of dimensions for each embedding vector.',
+ 'example' => 768
+ ])
+ ->addRule('embedding', [
+ 'type' => self::TYPE_FLOAT,
+ 'array' => true,
+ 'default' => [],
+ 'description' => 'Embedding vector values. If an error occurs, this will be an empty array.',
+ 'example' => [0.01, 0.02, 0.03]
+ ])
+ ->addRule('error', [
+ 'type' => self::TYPE_STRING,
+ 'array' => false,
+ 'default' => '',
+ 'description' => 'Error message if embedding generation fails. Empty string if no error.',
+ 'example' => 'Error message'
+ ]);
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/UsageDocumentsDB.php b/src/Appwrite/Utopia/Response/Model/UsageDocumentsDB.php
new file mode 100644
index 0000000000..099a9887b8
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/UsageDocumentsDB.php
@@ -0,0 +1,96 @@
+addRule('range', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Time range of the usage stats.',
+ 'default' => '',
+ 'example' => '30d',
+ ])
+ ->addRule('collectionsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of collections.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documents.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('storageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated storage used in bytes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databaseReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databaseWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('collections', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of collections per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documents', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of documents per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('storage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated storage used in bytes per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databaseReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database reads.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databaseWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database writes.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ;
+ }
+
+ public function getName(): string
+ {
+ return 'UsageDocumentsDB';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_USAGE_DOCUMENTSDB;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/UsageDocumentsDBs.php b/src/Appwrite/Utopia/Response/Model/UsageDocumentsDBs.php
new file mode 100644
index 0000000000..5ce229ce4a
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/UsageDocumentsDBs.php
@@ -0,0 +1,109 @@
+addRule('range', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Time range of the usage stats.',
+ 'default' => '',
+ 'example' => '30d',
+ ])
+ ->addRule('databasesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of DocumentsDB databases.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('collectionsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of collections.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documents.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('storageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of total databases storage in bytes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databasesReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of databases reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databasesWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of databases writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databases', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of databases per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('collections', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of collections per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documents', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of documents per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('storage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of the aggregated number of databases storage in bytes per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databasesReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database reads.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databasesWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database writes.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ;
+ }
+
+ public function getName(): string
+ {
+ return 'UsageDocumentsDBs';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_USAGE_DOCUMENTSDBS;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/UsageProject.php b/src/Appwrite/Utopia/Response/Model/UsageProject.php
index ee644aa845..e00c4bc1dc 100644
--- a/src/Appwrite/Utopia/Response/Model/UsageProject.php
+++ b/src/Appwrite/Utopia/Response/Model/UsageProject.php
@@ -18,7 +18,13 @@ class UsageProject extends Model
])
->addRule('documentsTotal', [
'type' => self::TYPE_INTEGER,
- 'description' => 'Total aggregated number of documents.',
+ 'description' => 'Total aggregated number of documents in legacy/tablesdb.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsdbDocumentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documents in documentsdb.',
'default' => 0,
'example' => 0,
])
@@ -34,12 +40,24 @@ class UsageProject extends Model
'default' => 0,
'example' => 0,
])
+ ->addRule('documentsdbTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documentsdb.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
->addRule('databasesStorageTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated sum of databases storage size (in bytes).',
'default' => 0,
'example' => 0,
])
+ ->addRule('documentsdbDatabasesStorageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated sum of documentsdb databases storage size (in bytes).',
+ 'default' => 0,
+ 'example' => 0,
+ ])
->addRule('usersTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Total aggregated number of users.',
@@ -100,6 +118,18 @@ class UsageProject extends Model
'default' => 0,
'example' => 0,
])
+ ->addRule('documentsdbDatabasesReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of documentsdb databases reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsdbDatabasesWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of documentsdb databases writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
->addRule('requests', [
'type' => Response::MODEL_METRIC,
'description' => 'Aggregated number of requests per period.',
@@ -203,6 +233,27 @@ class UsageProject extends Model
'example' => [],
'array' => true
])
+ ->addRule('documentsdbDatabasesReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of documentsdb database reads.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documentsdbDatabasesWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of documentsdb database writes.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documentsdbDatabasesStorage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated sum of documentsdb databases storage size (in bytes) per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
->addRule('imageTransformations', [
'type' => Response::MODEL_METRIC,
'description' => 'An array of aggregated number of image transformations.',
@@ -216,6 +267,133 @@ class UsageProject extends Model
'default' => 0,
'example' => 0,
])
+ // VectorsDB aggregates
+ ->addRule('vectorsdbDatabasesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB databases.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbCollectionsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB collections.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbDocumentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB documents.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbDatabasesStorageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated VectorsDB storage (bytes).',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbDatabasesReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbDatabasesWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('vectorsdbDatabases', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB databases per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('vectorsdbCollections', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB collections per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('vectorsdbDocuments', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB documents per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('vectorsdbDatabasesStorage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB storage per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('vectorsdbDatabasesReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB reads per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('vectorsdbDatabasesWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated VectorsDB writes per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('embeddingsText', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of text embedding calls per period.',
+ 'default' => [],
+ 'example' => []
+ ])
+ ->addRule('embeddingsTextTokens', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of tokens processed by text embeddings per period.',
+ 'default' => [],
+ 'example' => []
+ ])
+ ->addRule('embeddingsTextDuration', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated duration spent generating text embeddings per period.',
+ 'default' => [],
+ 'example' => []
+ ])
+ ->addRule('embeddingsTextErrors', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of errors while generating text embeddings per period.',
+ 'default' => [],
+ 'example' => []
+ ])
+ ->addRule('embeddingsTextTotal', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Total aggregated number of text embedding calls.',
+ 'default' => 0,
+ 'example' => 0
+ ])
+ ->addRule('embeddingsTextTokensTotal', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Total aggregated number of tokens processed by text.',
+ 'default' => 0,
+ 'example' => 0
+ ])
+ ->addRule('embeddingsTextDurationTotal', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Total aggregated duration spent generating text embeddings.',
+ 'default' => 0,
+ 'example' => 0
+ ])
+ ->addRule('embeddingsTextErrorsTotal', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Total aggregated number of errors while generating text embeddings.',
+ 'default' => 0,
+ 'example' => 0
+ ])
;
}
diff --git a/src/Appwrite/Utopia/Response/Model/UsageVectorsDB.php b/src/Appwrite/Utopia/Response/Model/UsageVectorsDB.php
new file mode 100644
index 0000000000..c652a3d62e
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/UsageVectorsDB.php
@@ -0,0 +1,96 @@
+addRule('range', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Time range of the usage stats.',
+ 'default' => '',
+ 'example' => '30d',
+ ])
+ ->addRule('collectionsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of collections.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documents.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('storageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated storage used in bytes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databaseReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databaseWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('collections', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of collections per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documents', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of documents per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('storage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated storage used in bytes per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databaseReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database reads.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databaseWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database writes.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ;
+ }
+
+ public function getName(): string
+ {
+ return 'UsageVectorsDB';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_USAGE_VECTORSDB;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/UsageVectorsDBs.php b/src/Appwrite/Utopia/Response/Model/UsageVectorsDBs.php
new file mode 100644
index 0000000000..1f5fe7853d
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/UsageVectorsDBs.php
@@ -0,0 +1,109 @@
+addRule('range', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Time range of the usage stats.',
+ 'default' => '',
+ 'example' => '30d',
+ ])
+ ->addRule('databasesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of VectorsDB databases.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('collectionsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of collections.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('documentsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated number of documents.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('storageTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total aggregated storage in bytes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databasesReadsTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database reads.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databasesWritesTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total number of database writes.',
+ 'default' => 0,
+ 'example' => 0,
+ ])
+ ->addRule('databases', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of databases per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('collections', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of collections per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('documents', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated number of documents per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('storage', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'Aggregated storage in bytes per period.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databasesReads', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database reads.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ->addRule('databasesWrites', [
+ 'type' => Response::MODEL_METRIC,
+ 'description' => 'An array of aggregated number of database writes.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true
+ ])
+ ;
+ }
+
+ public function getName(): string
+ {
+ return 'UsageVectorsDBs';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_USAGE_VECTORSDBS;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/VectorsDBCollection.php b/src/Appwrite/Utopia/Response/Model/VectorsDBCollection.php
new file mode 100644
index 0000000000..5053628e74
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/VectorsDBCollection.php
@@ -0,0 +1,41 @@
+addRule('dimension', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Embedding dimension.',
+ 'default' => 0,
+ 'example' => 1536,
+ ])
+ ->addRule('attributes', [
+ 'type' => [
+ Response::MODEL_ATTRIBUTE_OBJECT,
+ Response::MODEL_ATTRIBUTE_VECTOR,
+ ],
+ 'description' => 'Collection attributes.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
+ ;
+ }
+
+ public function getName(): string
+ {
+ return 'VectorsDB Collection';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_VECTORSDB_COLLECTION;
+ }
+}
diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php
index 0e484d4dcf..eea53d9ea8 100644
--- a/tests/e2e/General/UsageTest.php
+++ b/tests/e2e/General/UsageTest.php
@@ -3,6 +3,7 @@
namespace Tests\E2E\General;
use Appwrite\Platform\Modules\Compute\Specification;
+use Appwrite\Tests\Retry;
use CURLFile;
use DateTime;
use PHPUnit\Framework\Attributes\Depends;
@@ -599,6 +600,8 @@ class UsageTest extends Scope
$collectionsTotal = $data['collectionsTotal'];
$documentsTotal = $data['documentsTotal'];
+ sleep(self::WAIT);
+
$this->assertEventually(function () use ($requestsTotal, $databasesTotal, $documentsTotal) {
$response = $this->client->call(
Client::METHOD_GET,
@@ -923,6 +926,446 @@ class UsageTest extends Scope
}
#[Depends('testDatabaseStatsTablesAPI')]
+ public function testPrepareDocumentsDBStats(array $data): array
+ {
+ $documentsTotal = 0;
+ $collectionsTotal = 0;
+ $documentsDbTotal = 0;
+ $databasesTotal = $data['databasesTotal'];
+ $requestsTotal = $data['requestsTotal'];
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $name = uniqid() . ' documentsdb';
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/documentsdb',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'databaseId' => 'unique()',
+ 'name' => $name,
+ ]
+ );
+
+ $this->assertEquals($name, $response['body']['name']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $documentsDbTotal += 1;
+
+ $documentsDbId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/documentsdb/' . $documentsDbId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $documentsDbTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $name = uniqid() . ' collection';
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/documentsdb/' . $documentsDbId . '/collections',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'collectionId' => 'unique()',
+ 'name' => $name,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ );
+
+ $this->assertEquals($name, $response['body']['name']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $collectionsTotal += 1;
+
+ $collectionId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/documentsdb/' . $documentsDbId . '/collections/' . $collectionId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $collectionsTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/documentsdb/' . $documentsDbId . '/collections/' . $collectionId . '/documents',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'documentId' => 'unique()',
+ 'data' => [
+ 'name' => uniqid() . ' document',
+ 'value' => $i
+ ]
+ ]
+ );
+
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $documentsTotal += 1;
+
+ $documentId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/documentsdb/' . $documentsDbId . '/collections/' . $collectionId . '/documents/' . $documentId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $documentsTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ return array_merge($data, [
+ 'documentsDbId' => $documentsDbId,
+ 'documentsDbCollectionId' => $collectionId,
+ 'requestsTotal' => $requestsTotal,
+ 'databasesTotal' => $databasesTotal,
+ 'documentsDbTotal' => $documentsDbTotal,
+ 'documentsDbCollectionsTotal' => $collectionsTotal,
+ 'documentsDbDocumentsTotal' => $documentsTotal,
+ ]);
+ }
+
+ #[Depends('testPrepareDocumentsDBStats')]
+ #[Retry(count: 1)]
+ public function testDocumentsDBStats(array $data): array
+ {
+ $documentsDbId = $data['documentsDbId'];
+ $collectionId = $data['documentsDbCollectionId'];
+ $requestsTotal = $data['requestsTotal'];
+ $databasesTotal = $data['databasesTotal'];
+ $documentsDbTotal = $data['documentsDbTotal'];
+ $collectionsTotal = $data['documentsDbCollectionsTotal'];
+ $documentsTotal = $data['documentsDbDocumentsTotal'];
+
+ sleep(self::WAIT);
+
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/project/usage',
+ $this->getConsoleHeaders(),
+ [
+ 'period' => '1d',
+ 'startDate' => self::getToday(),
+ 'endDate' => self::getTomorrow(),
+ ]
+ );
+
+ $this->assertGreaterThanOrEqual(31, count($response['body']));
+ $this->assertCount(1, $response['body']['requests']);
+ $this->assertCount(1, $response['body']['network']);
+ $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
+ $this->validateDates($response['body']['requests']);
+ // documentsdbTotal should reflect only documents DB instances, not relational databases.
+ $this->assertEquals($documentsDbTotal, $response['body']['documentsdbTotal']);
+ $this->assertEquals($documentsTotal, $response['body']['documentsdbDocumentsTotal']);
+
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/databases/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($databasesTotal, $response['body']['databases'][array_key_last($response['body']['databases'])]['value']);
+ $this->validateDates($response['body']['databases']);
+
+ $this->assertEventually(function () use ($documentsDbId, $collectionsTotal, $documentsTotal) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/documentsdb/' . $documentsDbId . '/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($collectionsTotal, $response['body']['collections'][array_key_last($response['body']['collections'])]['value']);
+ $this->validateDates($response['body']['collections']);
+
+ $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']);
+ $this->validateDates($response['body']['documents']);
+ });
+
+ $this->assertEventually(function () use ($documentsDbId, $collectionId, $documentsTotal) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/documentsdb/' . $documentsDbId . '/collections/' . $collectionId . '/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']);
+ $this->validateDates($response['body']['documents']);
+ });
+
+ return $data;
+ }
+
+ #[Depends('testDocumentsDBStats')]
+ public function testPrepareVectorsDBStats(array $data): array
+ {
+ $documentsTotal = 0;
+ $collectionsTotal = 0;
+ $vectordbTotal = 0;
+ $databasesTotal = $data['databasesTotal'];
+ $requestsTotal = $data['requestsTotal'];
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $name = uniqid() . ' vectorsdb';
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/vectorsdb',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'databaseId' => 'unique()',
+ 'name' => $name,
+ ]
+ );
+
+ $this->assertEquals($name, $response['body']['name']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $vectordbTotal += 1;
+
+ $vectordbId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/vectorsdb/' . $vectordbId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $vectordbTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $name = uniqid() . ' collection';
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/vectorsdb/' . $vectordbId . '/collections',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'collectionId' => 'unique()',
+ 'name' => $name,
+ 'dimension' => 1536,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ );
+
+ $this->assertEquals($name, $response['body']['name']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $collectionsTotal += 1;
+
+ $collectionId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/vectorsdb/' . $vectordbId . '/collections/' . $collectionId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $collectionsTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ for ($i = 0; $i < self::CREATE; $i++) {
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/vectorsdb/' . $vectordbId . '/collections/' . $collectionId . '/documents',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ [
+ 'documentId' => 'unique()',
+ 'data' => [
+ 'embeddings' => array_fill(0, 1536, 0.1),
+ 'metadata' => [
+ 'name' => uniqid() . ' document',
+ 'value' => $i
+ ]
+ ]
+ ]
+ );
+
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $requestsTotal += 1;
+ $documentsTotal += 1;
+
+ $documentId = $response['body']['$id'];
+
+ if ($i < (self::CREATE / 2)) {
+ $response = $this->client->call(
+ Client::METHOD_DELETE,
+ '/vectorsdb/' . $vectordbId . '/collections/' . $collectionId . '/documents/' . $documentId,
+ array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()),
+ );
+
+ $this->assertEmpty($response['body']);
+
+ $documentsTotal -= 1;
+ $requestsTotal += 1;
+ }
+ }
+
+ return array_merge($data, [
+ 'vectordbId' => $vectordbId,
+ 'vectordbCollectionId' => $collectionId,
+ 'requestsTotal' => $requestsTotal,
+ 'databasesTotal' => $databasesTotal,
+ 'vectordbTotal' => $vectordbTotal,
+ 'vectordbCollectionsTotal' => $collectionsTotal,
+ 'vectordbDocumentsTotal' => $documentsTotal,
+ ]);
+ }
+
+ #[Depends('testPrepareVectorsDBStats')]
+ #[Retry(count: 1)]
+ public function testVectorsDBStats(array $data): array
+ {
+ $vectordbId = $data['vectordbId'];
+ $collectionId = $data['vectordbCollectionId'];
+ $requestsTotal = $data['requestsTotal'];
+ $databasesTotal = $data['databasesTotal'];
+ $vectordbTotal = $data['vectordbTotal'];
+ $collectionsTotal = $data['vectordbCollectionsTotal'];
+ $documentsTotal = $data['vectordbDocumentsTotal'];
+
+ $this->assertEventually(function () use ($requestsTotal, $vectordbTotal, $documentsTotal) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/project/usage',
+ $this->getConsoleHeaders(),
+ [
+ 'period' => '1d',
+ 'startDate' => self::getToday(),
+ 'endDate' => self::getTomorrow(),
+ ]
+ );
+
+ $this->assertGreaterThanOrEqual(31, count($response['body']));
+ $this->assertCount(1, $response['body']['requests']);
+ $this->assertCount(1, $response['body']['network']);
+ $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
+ $this->validateDates($response['body']['requests']);
+ // vectordbTotal should reflect only VectorsDB instances, not relational databases.
+ $this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']);
+ $this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']);
+ });
+
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/databases/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($databasesTotal, $response['body']['databases'][array_key_last($response['body']['databases'])]['value']);
+ $this->validateDates($response['body']['databases']);
+
+ $this->assertEventually(function () use ($vectordbId, $collectionsTotal, $documentsTotal) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/vectorsdb/' . $vectordbId . '/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($collectionsTotal, $response['body']['collections'][array_key_last($response['body']['collections'])]['value']);
+ $this->validateDates($response['body']['collections']);
+
+ $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']);
+ $this->validateDates($response['body']['documents']);
+ });
+
+ $this->assertEventually(function () use ($vectordbId, $collectionId, $documentsTotal) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/vectorsdb/' . $vectordbId . '/collections/' . $collectionId . '/usage?range=30d',
+ $this->getConsoleHeaders()
+ );
+
+ $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']);
+ $this->validateDates($response['body']['documents']);
+ });
+
+ return $data;
+ }
+
+ #[Depends('testVectorsDBStats')]
public function testPrepareFunctionsStats(array $data): array
{
$executionTime = 0;
@@ -1401,6 +1844,75 @@ class UsageTest extends Scope
});
}
+ public function testEmbeddingsTextUsageDoesNotBreakProjectUsage(): void
+ {
+ // Trigger embeddings endpoint a few times so stats usage worker has data to aggregate
+ for ($i = 0; $i < 3; $i++) {
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ '/vectorsdb/embeddings/text',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $this->getHeaders()),
+ [
+ 'model' => 'embeddinggemma',
+ 'texts' => [
+ 'usage test text ' . $i,
+ ],
+ ]
+ );
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertIsArray($response['body']['embeddings']);
+ $this->assertGreaterThan(0, $response['body']['total']);
+ }
+
+ // Ensure project usage endpoint still responds correctly after embeddings calls
+ $this->assertEventually(function () {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/project/usage',
+ $this->getConsoleHeaders(),
+ [
+ 'period' => '1h',
+ 'startDate' => self::getToday(),
+ 'endDate' => self::getTomorrow(),
+ ]
+ );
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertArrayHasKey('requests', $response['body']);
+ $this->assertArrayHasKey('network', $response['body']);
+ $this->assertArrayHasKey('executionsTotal', $response['body']);
+
+ // New embeddings metrics should be present after calls above
+ $this->assertArrayHasKey('embeddingsText', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextErrors', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextTokens', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextDuration', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextTotal', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextErrorsTotal', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextTokensTotal', $response['body']);
+ $this->assertArrayHasKey('embeddingsTextDurationTotal', $response['body']);
+
+ // Time-series arrays should be non-empty
+ $this->assertNotEmpty($response['body']['embeddingsText']);
+ $this->assertNotEmpty($response['body']['embeddingsTextTokens']);
+ $this->assertNotEmpty($response['body']['embeddingsTextDuration']);
+ $this->validateDates($response['body']['embeddingsText']);
+ $this->validateDates($response['body']['embeddingsTextTokens']);
+ $this->validateDates($response['body']['embeddingsTextDuration']);
+
+ // Total scalars should be greater than 0 (or >= 0 for errors)
+ $this->assertGreaterThan(0, $response['body']['embeddingsTextTotal']);
+ $this->assertGreaterThanOrEqual(0, $response['body']['embeddingsTextErrorsTotal']);
+ $this->assertGreaterThan(0, $response['body']['embeddingsTextTokensTotal']);
+ $this->assertGreaterThan(0, $response['body']['embeddingsTextDurationTotal']);
+ });
+ }
+
public function tearDown(): void
{
$this->projectId = '';
diff --git a/tests/e2e/Scopes/ApiDocumentsDB.php b/tests/e2e/Scopes/ApiDocumentsDB.php
new file mode 100644
index 0000000000..9948b03971
--- /dev/null
+++ b/tests/e2e/Scopes/ApiDocumentsDB.php
@@ -0,0 +1,109 @@
+getSupportForAttributes()) {
+ return;
+ }
$this->assertEventually(function () use ($databaseId, $containerId, $attributeKey) {
$attribute = $this->client->call(
Client::METHOD_GET,
diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php
index a8152ef77e..8c62c0c14a 100644
--- a/tests/e2e/Scopes/Scope.php
+++ b/tests/e2e/Scopes/Scope.php
@@ -144,6 +144,14 @@ abstract class Scope extends TestCase
return $this->getConsoleVariables()['supportForSchemas'] ?? true;
}
+ /**
+ * Check if the database adapter supports attributes
+ */
+ protected function getSupportForAttributes(): bool
+ {
+ return $this->getConsoleVariables()['supportForAttributes'] ?? true;
+ }
+
/**
* Get the maximum index length supported by the database adapter
*/
diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php
index ea387cff6c..107dceaa5e 100644
--- a/tests/e2e/Services/Account/AccountCustomClientTest.php
+++ b/tests/e2e/Services/Account/AccountCustomClientTest.php
@@ -2182,11 +2182,138 @@ class AccountCustomClientTest extends Scope
]), [
'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
- ]);
+ ], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2', $response['headers']['location']);
+
+ $oauthClient = new Client();
+ $oauthClient->setEndpoint('');
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/callback/mock/' . $this->getProject()['$id'] . '?code=', $response['headers']['location']);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/mock/redirect?code=', $response['headers']['location']);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+
+ $this->assertArrayHasKey('a_session_' . $this->getProject()['$id'] . '_legacy', $response['cookies']);
+ $this->assertArrayHasKey('a_session_' . $this->getProject()['$id'], $response['cookies']);
+
+ $oauthUserCookie = $response['cookies']['a_session_' . $this->getProject()['$id']];
+ $this->assertNotEmpty($oauthUserCookie);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
+ // Ensure user is authenticated
+ $response = $this->client->call(Client::METHOD_GET, '/account', [
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('useroauth@localhost.test', $response['body']['email']);
+
+ $oauthUserId = $response['body']['$id'];
+ $this->assertNotEmpty($oauthUserId);
+
+ // Ensure session looks as expected
+ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', [
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($oauthUserId, $response['body']['userId']);
+ $this->assertEquals('mock', $response['body']['provider']);
+
+ // Same sign-in again, but this time with oauth2 token flow
+ $response = $this->client->call(Client::METHOD_GET, '/account/tokens/oauth2/' . $provider, array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]), [
+ 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
+ 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
+ ], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2', $response['headers']['location']);
+
+ $oauthClient = new Client();
+ $oauthClient->setEndpoint('');
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/callback/mock/' . $this->getProject()['$id'] . '?code=', $response['headers']['location']);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/mock/redirect?code=', $response['headers']['location']);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertStringStartsWith('http://localhost/v1/mock/tests/general/oauth2/success?secret=', $response['headers']['location']);
+
+ $oauthParamsString = \parse_url($response['headers']['location'], PHP_URL_QUERY);
+ $oauthParams = [];
+ \parse_str($oauthParamsString, $oauthParams);
+
+ $this->assertNotEmpty($oauthParams['secret']);
+ $this->assertNotEmpty($oauthParams['userId']);
+
+ $response = $oauthClient->call(Client::METHOD_GET, $response['headers']['location'], followRedirects: false);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('success', $response['body']['result']);
+
+ // Claim session
+ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'userId' => $oauthParams['userId'],
+ 'secret' => $oauthParams['secret'],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertEquals('mock', $response['body']['provider']);
+
+ $this->assertArrayHasKey('a_session_' . $this->getProject()['$id'] . '_legacy', $response['cookies']);
+ $this->assertArrayHasKey('a_session_' . $this->getProject()['$id'], $response['cookies']);
+
+ $oauthUserCookie = $response['cookies']['a_session_' . $this->getProject()['$id']];
+ $this->assertNotEmpty($oauthUserCookie);
+
+ $response = $this->client->call(Client::METHOD_GET, '/account', [
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('useroauth@localhost.test', $response['body']['email']);
+
+ $oauthUserId = $response['body']['$id'];
+ $this->assertNotEmpty($oauthUserId);
+
+ // Ensure session looks as expected
+ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', [
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $oauthUserCookie,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($oauthUserId, $response['body']['userId']);
+ $this->assertEquals('mock', $response['body']['provider']);
+
/**
* Test for Failure when disabled
*/
diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php
index 5f8ac7dd94..3b44d5c1e3 100644
--- a/tests/e2e/Services/Databases/DatabasesBase.php
+++ b/tests/e2e/Services/Databases/DatabasesBase.php
@@ -68,6 +68,31 @@ trait DatabasesBase
return self::$databaseCache[$cacheKey];
}
+ /**
+ * Helper to create an attribute on a collection.
+ *
+ * @param string $databaseId
+ * @param string $collectionId
+ * @param string $type
+ * @param array $payload
+ *
+ * @return array
+ */
+ protected function createAttribute(string $databaseId, string $collectionId, string $type, array $payload): array
+ {
+ return $this->client->call(
+ Client::METHOD_POST,
+ $this->getSchemaUrl($databaseId, $collectionId) . '/' . $type,
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ $payload
+ );
+ }
+
+
/**
* Setup: Create database and collections
* Uses static caching to avoid recreating resources
@@ -150,75 +175,51 @@ trait DatabasesBase
$data = $this->setupCollection();
$databaseId = $data['databaseId'];
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ if (!$this->getSupportForAttributes()) {
+ self::$attributesCache[$cacheKey] = $data;
+ return self::$attributesCache[$cacheKey];
+ }
+ $title = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
'key' => 'title',
'size' => 256,
'required' => true,
]);
- $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $description = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
'key' => 'description',
'size' => 512,
'required' => false,
'default' => '',
]);
- $tagline = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $tagline = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
'key' => 'tagline',
'size' => 512,
'required' => false,
'default' => '',
]);
- $releaseYear = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $releaseYear = $this->createAttribute($databaseId, $data['moviesId'], 'integer', [
'key' => 'releaseYear',
'required' => true,
'min' => 1900,
'max' => 2200,
]);
- $duration = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $duration = $this->createAttribute($databaseId, $data['moviesId'], 'integer', [
'key' => 'duration',
'required' => false,
'min' => 60,
]);
- $actors = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $actors = $this->createAttribute($databaseId, $data['moviesId'], 'string', [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
- $datetime = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $data['moviesId']) . '/datetime', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
+ $datetime = $this->createAttribute($databaseId, $data['moviesId'], 'datetime', [
'key' => 'birthDay',
'required' => false,
]);
@@ -933,6 +934,10 @@ trait DatabasesBase
public function testCreateAttributes(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
// Use dedicated collections for this test to avoid conflicts with setupAttributes()
$data = $this->setupDatabase();
$databaseId = $data['databaseId'];
@@ -1182,6 +1187,10 @@ trait DatabasesBase
public function testListAttributes(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$response = $this->client->call(Client::METHOD_GET, $this->getSchemaUrl($databaseId, $data['moviesId']), array_merge([
@@ -1210,6 +1219,10 @@ trait DatabasesBase
public function testPatchAttribute(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$data = $this->setupDatabase();
$databaseId = $data['databaseId'];
@@ -1275,6 +1288,10 @@ trait DatabasesBase
public function testUpdateAttributeEnum(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1332,6 +1349,10 @@ trait DatabasesBase
public function testAttributeResponseModels(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
@@ -2043,65 +2064,75 @@ trait DatabasesBase
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
- // Create attributes needed for index testing
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/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']);
+ // Create attributes needed for index testing (only when supported).
+ // DocumentsDB can still create indexes without a predefined schema.
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $title['headers']['status-code']);
- $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'description', 'size' => 512, 'required' => false, 'default' => '']);
- $this->assertEquals(202, $description['headers']['status-code']);
+ $description = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'description',
+ 'size' => 512,
+ 'required' => false,
+ 'default' => '',
+ ]);
+ $this->assertEquals(202, $description['headers']['status-code']);
- $tagline = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'tagline', 'size' => 512, 'required' => false, 'default' => '']);
- $this->assertEquals(202, $tagline['headers']['status-code']);
+ $tagline = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'tagline',
+ 'size' => 512,
+ 'required' => false,
+ 'default' => '',
+ ]);
+ $this->assertEquals(202, $tagline['headers']['status-code']);
- $releaseYear = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'releaseYear', 'required' => true, 'min' => 1900, 'max' => 2200]);
- $this->assertEquals(202, $releaseYear['headers']['status-code']);
+ $releaseYear = $this->createAttribute($databaseId, $collectionId, 'integer', [
+ 'key' => 'releaseYear',
+ 'required' => true,
+ 'min' => 1900,
+ 'max' => 2200,
+ ]);
+ $this->assertEquals(202, $releaseYear['headers']['status-code']);
- $actors = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'actors', 'size' => 256, 'required' => false, 'array' => true]);
- $this->assertEquals(202, $actors['headers']['status-code']);
+ $actors = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'actors',
+ 'size' => 256,
+ 'required' => false,
+ 'array' => true,
+ ]);
+ $this->assertEquals(202, $actors['headers']['status-code']);
- $birthDay = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/datetime', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'birthDay', 'required' => false]);
- $this->assertEquals(202, $birthDay['headers']['status-code']);
+ $birthDay = $this->createAttribute($databaseId, $collectionId, 'datetime', [
+ 'key' => 'birthDay',
+ 'required' => false,
+ ]);
+ $this->assertEquals(202, $birthDay['headers']['status-code']);
- $integers = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'integers', 'required' => false, 'array' => true, 'min' => 10, 'max' => 99]);
- $this->assertEquals(202, $integers['headers']['status-code']);
+ $integers = $this->createAttribute($databaseId, $collectionId, 'integer', [
+ 'key' => 'integers',
+ 'required' => false,
+ 'array' => true,
+ 'min' => 10,
+ 'max' => 99,
+ ]);
+ $this->assertEquals(202, $integers['headers']['status-code']);
- $integers2 = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), ['key' => 'integers2', 'required' => false, 'array' => true, 'min' => 10, 'max' => 99]);
- $this->assertEquals(202, $integers2['headers']['status-code']);
+ $integers2 = $this->createAttribute($databaseId, $collectionId, 'integer', [
+ 'key' => 'integers2',
+ 'required' => false,
+ 'array' => true,
+ 'min' => 10,
+ 'max' => 99,
+ ]);
+ $this->assertEquals(202, $integers2['headers']['status-code']);
- // Wait for attributes to be ready
- $this->waitForAllAttributes($databaseId, $collectionId);
+ // Wait for attributes to be ready
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
$titleIndex = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
@@ -2218,13 +2249,16 @@ trait DatabasesBase
$this->getIndexAttributesParam() => ['description', 'tagline'],
]);
- if ($this->getMaxIndexLength() < 1024) {
- // Only SQL-based adapters (MariaDB, PostgreSQL) enforce byte-level index length limits
- $this->assertEquals(400, $tooLong['headers']['status-code']);
- $this->assertStringContainsString('Index length is longer than the maximum', $tooLong['body']['message']);
- } else {
- // MongoDB (maxIndexLength=1024) doesn't exceed the limit with 512+512
- $this->assertEquals(202, $tooLong['headers']['status-code']);
+ // documentsdb isn't aware of the size so it will create
+ if ($this->getSupportForAttributes()) {
+ if ($this->getMaxIndexLength() < 1024) {
+ // Only SQL-based adapters (MariaDB, PostgreSQL) enforce byte-level index length limits
+ $this->assertEquals(400, $tooLong['headers']['status-code']);
+ $this->assertStringContainsString('Index length is longer than the maximum', $tooLong['body']['message']);
+ } else {
+ // MongoDB (maxIndexLength=1024) doesn't exceed the limit with 512+512
+ $this->assertEquals(202, $tooLong['headers']['status-code']);
+ }
}
$fulltextArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
@@ -2238,119 +2272,126 @@ trait DatabasesBase
]);
$this->assertEquals(400, $fulltextArray['headers']['status-code']);
- $this->assertEquals('Creating indexes on array attributes is not currently supported.', $fulltextArray['body']['message']);
+ $errorMessage = $this->getSupportForAttributes() ? "Creating indexes on array attributes is not currently supported." : "There is already a fulltext index in the collection";
+ $this->assertEquals($errorMessage, $fulltextArray['body']['message']);
- $actorsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'key' => 'index-actors',
- 'type' => 'key',
- $this->getIndexAttributesParam() => ['actors'],
- ]);
+ if ($this->getSupportForAttributes()) {
- $this->assertEquals(400, $actorsArray['headers']['status-code']);
- $this->assertEquals('Creating indexes on array attributes is not currently supported.', $actorsArray['body']['message']);
+ $actorsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'key' => 'index-actors',
+ 'type' => 'key',
+ $this->getIndexAttributesParam() => ['actors'],
+ ]);
- $twoLevelsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'key' => 'index-ip-actors',
- 'type' => 'key',
- $this->getIndexAttributesParam() => ['releaseYear', 'actors'], // 2 levels
- 'orders' => ['DESC', 'DESC'],
- ]);
+ $this->assertEquals(400, $actorsArray['headers']['status-code']);
+ $this->assertEquals('Creating indexes on array attributes is not currently supported.', $actorsArray['body']['message']);
- $this->assertEquals(400, $twoLevelsArray['headers']['status-code']);
- $this->assertEquals('Creating indexes on array attributes is not currently supported.', $twoLevelsArray['body']['message']);
+ $twoLevelsArray = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'key' => 'index-ip-actors',
+ 'type' => 'key',
+ $this->getIndexAttributesParam() => ['releaseYear', 'actors'], // 2 levels
+ 'orders' => ['DESC', 'DESC'],
+ ]);
- $unknown = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'key' => 'index-unknown',
- 'type' => 'key',
- $this->getIndexAttributesParam() => ['Unknown'],
- ]);
+ $this->assertEquals(400, $twoLevelsArray['headers']['status-code']);
+ $this->assertEquals('Creating indexes on array attributes is not currently supported.', $twoLevelsArray['body']['message']);
- $this->assertEquals(400, $unknown['headers']['status-code']);
- $this->assertStringContainsString('\'Unknown\' required for the index could not be found', $unknown['body']['message']);
+ $unknown = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'key' => 'index-unknown',
+ 'type' => 'key',
+ $this->getIndexAttributesParam() => ['Unknown'],
+ ]);
- $index1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'key' => 'integers-order',
- 'type' => 'key',
- $this->getIndexAttributesParam() => ['integers'], // array attribute
- 'orders' => ['DESC'], // Check order is removed in API
- ]);
+ $this->assertEquals(400, $unknown['headers']['status-code']);
+ $this->assertStringContainsString('\'Unknown\' required for the index could not be found', $unknown['body']['message']);
+ $index1 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'key' => 'integers-order',
+ 'type' => 'key',
+ $this->getIndexAttributesParam() => ['integers'], // array attribute
+ 'orders' => ['DESC'], // Check order is removed in API
+ ]);
- $this->assertEquals(400, $index1['headers']['status-code']);
- $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index1['body']['message']);
+ $this->assertEquals(400, $index1['headers']['status-code']);
+ $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index1['body']['message']);
- $index2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'key' => 'integers-size',
- 'type' => 'key',
- $this->getIndexAttributesParam() => ['integers2'], // array attribute
- ]);
+ $index2 = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'key' => 'integers-size',
+ 'type' => 'key',
+ $this->getIndexAttributesParam() => ['integers2'], // array attribute
+ ]);
- $this->assertEquals(400, $index2['headers']['status-code']);
- $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index2['body']['message']);
+ $this->assertEquals(400, $index2['headers']['status-code']);
+ $this->assertEquals('Creating indexes on array attributes is not currently supported.', $index2['body']['message']);
- if (!$this->getSupportForMultipleFulltextIndexes()) {
- // Some databases only allow one fulltext index per collection
- $this->assertEquals('There is already a fulltext index in the collection', $fulltextReleaseYear['body']['message']);
- } else {
- $this->assertEquals('Attribute "releaseYear" cannot be part of a fulltext index, must be of type string', $fulltextReleaseYear['body']['message']);
- }
+ if (!$this->getSupportForMultipleFulltextIndexes()) {
+ // Some databases only allow one fulltext index per collection
+ $this->assertEquals('There is already a fulltext index in the collection', $fulltextReleaseYear['body']['message']);
+ } else {
+ $this->assertEquals('Attribute "releaseYear" cannot be part of a fulltext index, must be of type string', $fulltextReleaseYear['body']['message']);
+ }
- /**
- * Create Indexes by worker
- */
- $this->waitForAllIndexes($databaseId, $collectionId);
+ /**
+ * Create Indexes by worker
+ */
+ $this->waitForAllIndexes($databaseId, $collectionId);
- $collectionResponse = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), []);
-
- $this->assertIsArray($collectionResponse['body']['indexes']);
- $expectedIndexCount = $this->getMaxIndexLength() < 1024 ? 4 : 5; // MongoDB accepts tooLong index
- $this->assertCount($expectedIndexCount, $collectionResponse['body']['indexes']);
- $indexKeys = array_column($collectionResponse['body']['indexes'], 'key');
- $this->assertContains($titleIndex['body']['key'], $indexKeys);
- $this->assertContains($releaseYearIndex['body']['key'], $indexKeys);
- $this->assertContains($releaseWithDate1['body']['key'], $indexKeys);
- $this->assertContains($releaseWithDate2['body']['key'], $indexKeys);
-
- $this->assertEventually(function () use ($databaseId, $collectionId) {
- $collResp = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
+ $collectionResponse = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
- ]));
+ ]), []);
- foreach ($collResp['body']['indexes'] as $index) {
- $this->assertEquals('available', $index['status']);
- }
+ $this->assertIsArray($collectionResponse['body']['indexes']);
+ $expectedIndexCount = $this->getMaxIndexLength() < 1024 ? 4 : 5; // MongoDB accepts tooLong index
+ $this->assertCount($expectedIndexCount, $collectionResponse['body']['indexes']);
+ $indexKeys = array_column($collectionResponse['body']['indexes'], 'key');
+ $this->assertContains($titleIndex['body']['key'], $indexKeys);
+ $this->assertContains($releaseYearIndex['body']['key'], $indexKeys);
+ $this->assertContains($releaseWithDate1['body']['key'], $indexKeys);
+ $this->assertContains($releaseWithDate2['body']['key'], $indexKeys);
- return true;
- }, 60000, 500);
+ $this->assertEventually(function () use ($databaseId, $collectionId) {
+ $collResp = $this->client->call(Client::METHOD_GET, $this->getContainerUrl($databaseId, $collectionId), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+
+ foreach ($collResp['body']['indexes'] as $index) {
+ $this->assertEquals('available', $index['status']);
+ }
+
+ return true;
+ }, 60000, 500);
+ }
}
public function testGetIndexByKeyWithLengths(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$collectionId = $data['moviesId'];
@@ -2544,6 +2585,7 @@ trait DatabasesBase
$this->getRecordIdParam() => ID::unique(),
'data' => [
'releaseYear' => 2020, // Missing title, expect an 400 error
+ 'birthDay' => null // adding null here as documentsdb will require it as for documentsdb this document will be created
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
@@ -2563,7 +2605,11 @@ trait DatabasesBase
$this->assertCount(2, $document1['body']['actors']);
$this->assertEquals($document1['body']['actors'][0], 'Chris Evans');
$this->assertEquals($document1['body']['actors'][1], 'Samuel Jackson');
- $this->assertEquals($document1['body']['birthDay'], '1975-06-12T12:12:55.000+00:00');
+ if ($this->getSupportForAttributes()) {
+ $this->assertEquals($document1['body']['birthDay'], '1975-06-12T12:12:55.000+00:00');
+ } else {
+ $this->assertEquals($document1['body']['birthDay'], '1975-06-12 14:12:55+02:00');
+ }
$this->assertTrue(array_key_exists('$sequence', $document1['body']));
$this->assertIsString($document1['body']['$sequence']);
@@ -2598,10 +2644,18 @@ trait DatabasesBase
$this->assertCount(2, $document3['body']['actors']);
$this->assertEquals($document3['body']['actors'][0], 'Tom Holland');
$this->assertEquals($document3['body']['actors'][1], 'Zendaya Maree Stoermer');
- $this->assertEquals($document3['body']['birthDay'], '1975-06-12T18:12:55.000+00:00'); // UTC for NY
+ if ($this->getSupportForAttributes()) {
+ $this->assertEquals($document3['body']['birthDay'], '1975-06-12T18:12:55.000+00:00'); // UTC for NY
+ } else {
+ $this->assertEquals($document1['body']['birthDay'], '1975-06-12 14:12:55+02:00');
+ }
$this->assertTrue(array_key_exists('$sequence', $document3['body']));
- $this->assertEquals(400, $document4['headers']['status-code']);
+ if ($this->getSupportForAttributes()) {
+ $this->assertEquals(400, $document4['headers']['status-code']);
+ } else {
+ $this->assertEquals(201, $document4['headers']['status-code']);
+ }
}
public function testUpsertDocument(): void
@@ -2752,6 +2806,10 @@ trait DatabasesBase
$this->assertEquals(204, $document['headers']['status-code']);
// relationship behaviour - only test on databases that support relationships
+ /** @var array|null $person */
+ $person = null;
+ /** @var array|null $library */
+ $library = null;
if ($this->getSupportForRelationships()) {
$person = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
'content-type' => 'application/json',
@@ -3073,7 +3131,7 @@ trait DatabasesBase
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
// upsertion for the related document without passing permissions - only for databases that support relationships
- if ($this->getSupportForRelationships()) {
+ if ($this->getSupportForRelationships() && $person !== null && $library !== null) {
// data should get added
$newPersonId = ID::unique();
$personNoPerm = $this->client->call(Client::METHOD_PUT, $this->getRecordUrl($databaseId, $person['body']['$id'], $newPersonId), array_merge([
@@ -3264,6 +3322,10 @@ trait DatabasesBase
public function testListDocumentsWithCache(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$data = $this->setupDocuments();
$databaseId = $data['databaseId'];
$docIds = $data['documentIds'];
@@ -3394,6 +3456,10 @@ trait DatabasesBase
public function testListDocumentsCacheBustedByAttributeChange(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$data = $this->setupDocuments();
$databaseId = $data['databaseId'];
$docIds = $data['documentIds'];
@@ -4030,34 +4096,43 @@ trait DatabasesBase
]);
$this->assertEquals(200, $documents['headers']['status-code']);
- $this->assertEquals(0, $documents['body']['total']);
- $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [
- Query::greaterThan('birthDay', '16/01/2024 12:00:00AM')->toString(),
- ],
- ]);
+ // for tablesdb/legacy it is full match , for docsdb inner pattern is matched
+ if ($this->getSupportForAttributes()) {
+ $this->assertEquals(0, $documents['body']['total']);
+ } else {
+ $this->assertGreaterThan(0, $documents['body']['total']);
+ }
- $this->assertEquals(400, $documents['headers']['status-code']);
- $this->assertEquals('Invalid query: Query value is invalid for attribute "birthDay"', $documents['body']['message']);
+ if ($this->getSupportForAttributes()) {
+ $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::greaterThan('birthDay', '16/01/2024 12:00:00AM')->toString(),
+ ],
+ ]);
- $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [
- Query::greaterThan('birthDay', '1960-01-01 10:10:10+02:30')->toString(),
- ],
- ]);
+ $this->assertEquals(400, $documents['headers']['status-code']);
+ $this->assertEquals('Invalid query: Query value is invalid for attribute "birthDay"', $documents['body']['message']);
+
+ $documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::greaterThan('birthDay', '1960-01-01 10:10:10+02:30')->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(2, count($documents['body'][$this->getRecordResource()]));
+ $birthDays = array_column($documents['body'][$this->getRecordResource()], 'birthDay');
+ $this->assertContains('1975-06-12T12:12:55.000+00:00', $birthDays);
+ $this->assertContains('1975-06-12T18:12:55.000+00:00', $birthDays);
+ }
- $this->assertEquals(200, $documents['headers']['status-code']);
- $this->assertGreaterThanOrEqual(2, count($documents['body'][$this->getRecordResource()]));
- $birthDays = array_column($documents['body'][$this->getRecordResource()], 'birthDay');
- $this->assertContains('1975-06-12T12:12:55.000+00:00', $birthDays);
- $this->assertContains('1975-06-12T18:12:55.000+00:00', $birthDays);
$documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
'content-type' => 'application/json',
@@ -4701,6 +4776,10 @@ trait DatabasesBase
public function testInvalidDocumentStructure(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('Attributes are not supported by this database adapter');
+ return;
+ }
$database = $this->client->call(Client::METHOD_POST, $this->getApiBasePath(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -5401,22 +5480,18 @@ trait DatabasesBase
$this->assertEquals($collection['body'][$this->getSecurityResponseKey()], true);
$collectionId = $collection['body']['$id'];
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'attribute',
+ 'size' => 64,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $attribute['headers']['status-code'], 202);
+ $this->assertEquals('attribute', $attribute['body']['key']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'attribute',
- 'size' => 64,
- 'required' => true,
- ]);
-
- $this->assertEquals(202, $attribute['headers']['status-code'], 202);
- $this->assertEquals('attribute', $attribute['body']['key']);
-
- // wait for db to add attribute
- $this->waitForAttribute($databaseId, $collectionId, 'attribute');
+ // wait for db to add attribute
+ $this->waitForAttribute($databaseId, $collectionId, 'attribute');
+ }
$index = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
@@ -5425,7 +5500,7 @@ trait DatabasesBase
]), [
'key' => 'key_attribute',
'type' => 'key',
- $this->getIndexAttributesParam() => [$attribute['body']['key']],
+ $this->getIndexAttributesParam() => ['attribute'],
]);
$this->assertEquals(202, $index['headers']['status-code']);
@@ -5591,21 +5666,17 @@ trait DatabasesBase
$this->assertEquals($collection['body'][$this->getSecurityResponseKey()], false);
$collectionId = $collection['body']['$id'];
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->createAttribute($databaseId, $collectionId, 'string', [
+ 'key' => 'attribute',
+ 'size' => 64,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $attribute['headers']['status-code'], 202);
+ $this->assertEquals('attribute', $attribute['body']['key']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'attribute',
- 'size' => 64,
- 'required' => true,
- ]);
-
- $this->assertEquals(202, $attribute['headers']['status-code'], 202);
- $this->assertEquals('attribute', $attribute['body']['key']);
-
- $this->waitForAttribute($databaseId, $collectionId, 'attribute');
+ $this->waitForAttribute($databaseId, $collectionId, 'attribute');
+ }
$index = $this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
@@ -5614,7 +5685,7 @@ trait DatabasesBase
]), [
'key' => 'key_attribute',
'type' => 'key',
- $this->getIndexAttributesParam() => [$attribute['body']['key']],
+ $this->getIndexAttributesParam() => ['attribute'],
]);
$this->assertEquals(202, $index['headers']['status-code'], 'Index creation failed: ' . json_encode($index['body'] ?? []));
@@ -5981,21 +6052,18 @@ trait DatabasesBase
$moviesId = $movies['body']['$id'];
// create attribute
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $moviesId) . '/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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $moviesId, 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
- $this->assertEquals(202, $title['headers']['status-code']);
-
- // wait for database worker to create attributes
- $this->waitForAttribute($databaseId, $moviesId, 'title');
+ $this->assertEquals(202, $title['headers']['status-code']);
+ // wait for database worker to create attributes
+ $this->waitForAttribute($databaseId, $moviesId, 'title');
+ }
// add document
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $moviesId), array_merge([
'content-type' => 'application/json',
@@ -6060,6 +6128,11 @@ trait DatabasesBase
public function testAttributeBooleanDefault(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
$data = $this->setupDatabase();
$databaseId = $data['databaseId'];
@@ -6946,32 +7019,25 @@ trait DatabasesBase
$this->assertEquals(201, $presidents['headers']['status-code']);
$this->assertEquals($presidents['body']['name'], 'USA Presidents');
- // Create Attributes
- $firstName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $presidents['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'first_name',
- 'size' => 256,
- 'required' => true,
- ]);
- $this->assertEquals(202, $firstName['headers']['status-code']);
+ // Create Attributes (only for adapters that support attributes)
+ if ($this->getSupportForAttributes()) {
+ $firstName = $this->createAttribute($databaseId, $presidents['body']['$id'], 'string', [
+ 'key' => 'first_name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $firstName['headers']['status-code']);
- $lastName = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $presidents['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'last_name',
- 'size' => 256,
- 'required' => true,
- ]);
+ $lastName = $this->createAttribute($databaseId, $presidents['body']['$id'], 'string', [
+ 'key' => 'last_name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $lastName['headers']['status-code']);
- $this->assertEquals(202, $lastName['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $presidents['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $presidents['body']['$id']);
+ }
$document1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $presidents['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -7176,20 +7242,19 @@ trait DatabasesBase
'databaseId' => $databaseId,
];
- $longtext = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($data['databaseId'], $data['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'longtext',
- 'size' => 100000000,
- 'required' => false,
- 'default' => null,
- ]);
+ // Create attribute only on adapters that support attributes; DocumentsDB can still store the field schemalessly
+ if ($this->getSupportForAttributes()) {
+ $longtext = $this->createAttribute($data['databaseId'], $data['$id'], 'string', [
+ 'key' => 'longtext',
+ 'size' => 100000000,
+ 'required' => false,
+ 'default' => null,
+ ]);
- $this->assertEquals($longtext['headers']['status-code'], 202);
+ $this->assertEquals(202, $longtext['headers']['status-code']);
- $this->waitForAttribute($data['databaseId'], $data['$id'], 'longtext');
+ $this->waitForAttribute($data['databaseId'], $data['$id'], 'longtext');
+ }
for ($i = 0; $i < 10; $i++) {
$this->client->call(Client::METHOD_POST, $this->getRecordUrl($data['databaseId'], $data['$id']), array_merge([
@@ -7257,17 +7322,20 @@ trait DatabasesBase
]);
$collectionId = $collection['body']['$id'];
- // Add integer attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'count',
- 'required' => true,
- ]);
+ // Add integer attribute only when supported; schemaless adapters (e.g. documentsdb)
+ // can still store the field without a predefined attribute.
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'count',
+ 'required' => true,
+ ]);
- $this->waitForAttribute($databaseId, $collectionId, 'count');
+ $this->waitForAttribute($databaseId, $collectionId, 'count');
+ }
// Create document with initial count = 5
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
@@ -7332,7 +7400,10 @@ trait DatabasesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
- $this->assertEquals(404, $notFound['headers']['status-code']);
+ $this->assertEquals(
+ $this->getSupportForAttributes() ? 404 : 200,
+ $notFound['headers']['status-code']
+ );
// Test increment with value 0
$inc3 = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, $docId) . "/count/increment", array_merge([
@@ -7373,17 +7444,20 @@ trait DatabasesBase
$collectionId = $collection['body']['$id'];
- // Add integer attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'count',
- 'required' => true,
- ]);
+ // Add integer attribute only when supported; schemaless adapters can still
+ // store the field without a predefined attribute.
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId) . '/integer', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'count',
+ 'required' => true,
+ ]);
- $this->waitForAttribute($databaseId, $collectionId, 'count');
+ $this->waitForAttribute($databaseId, $collectionId, 'count');
+ }
// Create document with initial count = 10
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
@@ -9805,32 +9879,25 @@ trait DatabasesBase
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals($movies['body']['name'], 'Movies');
- // Create Attributes
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $movies['body']['$id']) . '/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']);
+ // Create Attributes (only when supported; DocumentsDB can still store fields schemalessly)
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $movies['body']['$id'], 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $title['headers']['status-code']);
- $genre = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $movies['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'genre',
- 'size' => 256,
- 'required' => true,
- ]);
+ $genre = $this->createAttribute($databaseId, $movies['body']['$id'], 'string', [
+ 'key' => 'genre',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $genre['headers']['status-code']);
- $this->assertEquals(202, $genre['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $movies['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $movies['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $movies['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -9973,31 +10040,24 @@ trait DatabasesBase
$this->assertEquals(201, $products['headers']['status-code']);
$this->assertEquals($products['body']['name'], 'Products');
- // Create Attributes
- $name = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $products['body']['$id']) . '/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, $name['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $name = $this->createAttribute($databaseId, $products['body']['$id'], 'string', [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $name['headers']['status-code']);
- $price = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $products['body']['$id']) . '/float', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'price',
- 'required' => true,
- ]);
+ $price = $this->createAttribute($databaseId, $products['body']['$id'], 'float', [
+ 'key' => 'price',
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $price['headers']['status-code']);
- $this->assertEquals(202, $price['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $products['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $products['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -10105,32 +10165,25 @@ trait DatabasesBase
$this->assertEquals(201, $employees['headers']['status-code']);
$this->assertEquals($employees['body']['name'], 'Employees');
- // Create Attributes
- $name = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $employees['body']['$id']) . '/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, $name['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $name = $this->createAttribute($databaseId, $employees['body']['$id'], 'string', [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $name['headers']['status-code']);
- $department = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $employees['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'department',
- 'size' => 256,
- 'required' => true,
- ]);
+ $department = $this->createAttribute($databaseId, $employees['body']['$id'], 'string', [
+ 'key' => 'department',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $department['headers']['status-code']);
- $this->assertEquals(202, $department['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $employees['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $employees['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $employees['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -10238,32 +10291,25 @@ trait DatabasesBase
$this->assertEquals(201, $files['headers']['status-code']);
$this->assertEquals($files['body']['name'], 'Files');
- // Create Attributes
- $filename = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $files['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'filename',
- 'size' => 256,
- 'required' => true,
- ]);
- $this->assertEquals(202, $filename['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $filename = $this->createAttribute($databaseId, $files['body']['$id'], 'string', [
+ 'key' => 'filename',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $filename['headers']['status-code']);
- $type = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $files['body']['$id']) . '/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,
- ]);
+ $type = $this->createAttribute($databaseId, $files['body']['$id'], 'string', [
+ 'key' => 'type',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $type['headers']['status-code']);
- $this->assertEquals(202, $type['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $files['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $files['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $files['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -10371,32 +10417,25 @@ trait DatabasesBase
$this->assertEquals(201, $posts['headers']['status-code']);
$this->assertEquals($posts['body']['name'], 'Posts');
- // Create Attributes
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $posts['body']['$id']) . '/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']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $posts['body']['$id'], 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $title['headers']['status-code']);
- $content = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $posts['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'content',
- 'size' => 512,
- 'required' => true,
- ]);
+ $content = $this->createAttribute($databaseId, $posts['body']['$id'], 'string', [
+ 'key' => 'content',
+ 'size' => 512,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $content['headers']['status-code']);
- $this->assertEquals(202, $content['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $posts['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $posts['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $posts['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -10511,32 +10550,25 @@ trait DatabasesBase
$this->assertEquals(201, $events['headers']['status-code']);
$this->assertEquals($events['body']['name'], 'Events');
- // Create Attributes
- $name = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $events['body']['$id']) . '/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, $name['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $name = $this->createAttribute($databaseId, $events['body']['$id'], 'string', [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $name['headers']['status-code']);
- $description = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $events['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'description',
- 'size' => 512,
- 'required' => true,
- ]);
+ $description = $this->createAttribute($databaseId, $events['body']['$id'], 'string', [
+ 'key' => 'description',
+ 'size' => 512,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $description['headers']['status-code']);
- $this->assertEquals(202, $description['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $events['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $events['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $events['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -10651,31 +10683,25 @@ trait DatabasesBase
$this->assertEquals(201, $articles['headers']['status-code']);
$this->assertEquals($articles['body']['name'], 'Articles');
- // Create Attributes
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $articles['body']['$id']) . '/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']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $articles['body']['$id'], 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $title['headers']['status-code']);
- $content = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $articles['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'content',
- 'size' => 5000,
- 'required' => true,
- ]);
- $this->assertEquals(202, $content['headers']['status-code']);
+ $content = $this->createAttribute($databaseId, $articles['body']['$id'], 'string', [
+ 'key' => 'content',
+ 'size' => 5000,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $content['headers']['status-code']);
- // Wait for attributes to be available
- $this->waitForAllAttributes($databaseId, $articles['body']['$id']);
+ // Wait for attributes to be available
+ $this->waitForAllAttributes($databaseId, $articles['body']['$id']);
+ }
// Create first article
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $articles['body']['$id']), array_merge([
@@ -10836,32 +10862,25 @@ trait DatabasesBase
$this->assertEquals(201, $tasks['headers']['status-code']);
$this->assertEquals($tasks['body']['name'], 'Tasks');
- // Create Attributes
- $title = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tasks['body']['$id']) . '/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']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $title = $this->createAttribute($databaseId, $tasks['body']['$id'], 'string', [
+ 'key' => 'title',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $title['headers']['status-code']);
- $status = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tasks['body']['$id']) . '/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,
- ]);
+ $status = $this->createAttribute($databaseId, $tasks['body']['$id'], 'string', [
+ 'key' => 'status',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $status['headers']['status-code']);
- $this->assertEquals(202, $status['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $tasks['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $tasks['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tasks['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -11009,32 +11028,25 @@ trait DatabasesBase
$this->assertEquals(201, $orders['headers']['status-code']);
$this->assertEquals($orders['body']['name'], 'Orders');
- // Create Attributes
- $orderNumber = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $orders['body']['$id']) . '/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'orderNumber',
- 'size' => 256,
- 'required' => true,
- ]);
- $this->assertEquals(202, $orderNumber['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $orderNumber = $this->createAttribute($databaseId, $orders['body']['$id'], 'string', [
+ 'key' => 'orderNumber',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $orderNumber['headers']['status-code']);
- $status = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $orders['body']['$id']) . '/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,
- ]);
+ $status = $this->createAttribute($databaseId, $orders['body']['$id'], 'string', [
+ 'key' => 'status',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $status['headers']['status-code']);
- $this->assertEquals(202, $status['headers']['status-code']);
-
- // Wait for worker
- $this->waitForAllAttributes($databaseId, $orders['body']['$id']);
+ // Wait for worker
+ $this->waitForAllAttributes($databaseId, $orders['body']['$id']);
+ }
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $orders['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -11181,30 +11193,24 @@ trait DatabasesBase
$this->assertEquals(201, $products['headers']['status-code']);
$this->assertEquals($products['body']['name'], 'Products');
- // Create Attributes
- $name = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $products['body']['$id']) . '/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, $name['headers']['status-code']);
+ // Create Attributes (only when supported)
+ if ($this->getSupportForAttributes()) {
+ $name = $this->createAttribute($databaseId, $products['body']['$id'], 'string', [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $name['headers']['status-code']);
- $price = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $products['body']['$id']) . '/float', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'price',
- 'required' => true,
- ]);
- $this->assertEquals(202, $price['headers']['status-code']);
+ $price = $this->createAttribute($databaseId, $products['body']['$id'], 'float', [
+ 'key' => 'price',
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $price['headers']['status-code']);
- // Wait for attributes to be available
- $this->waitForAllAttributes($databaseId, $products['body']['$id']);
+ // Wait for attributes to be available
+ $this->waitForAllAttributes($databaseId, $products['body']['$id']);
+ }
// Create first product
$row1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $products['body']['$id']), array_merge([
diff --git a/tests/e2e/Services/Databases/DocumentsDB/DocumentsDBIndexTest.php b/tests/e2e/Services/Databases/DocumentsDB/DocumentsDBIndexTest.php
new file mode 100644
index 0000000000..1fdcc84d0c
--- /dev/null
+++ b/tests/e2e/Services/Databases/DocumentsDB/DocumentsDBIndexTest.php
@@ -0,0 +1,362 @@
+client->call(
+ 'POST',
+ '/documentsdb',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'databaseId' => ID::unique(),
+ 'name' => 'DocumentsDB Indexes',
+ ]
+ );
+
+ $this->assertNotEmpty($database['body']['$id']);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $movies = $this->client->call(
+ 'POST',
+ '/documentsdb/' . $databaseId . '/collections',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'documentSecurity' => true,
+ ]
+ );
+
+ $this->assertEquals(201, $movies['headers']['status-code']);
+ $moviesId = $movies['body']['$id'];
+
+ $titleIndex = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'titleIndex',
+ 'type' => 'fulltext',
+ 'attributes' => ['title'],
+ ]);
+
+ $this->assertEquals(202, $titleIndex['headers']['status-code']);
+ $this->assertEquals('titleIndex', $titleIndex['body']['key']);
+ $this->assertEquals('fulltext', $titleIndex['body']['type']);
+ $this->assertCount(1, $titleIndex['body']['attributes']);
+ $this->assertEquals('title', $titleIndex['body']['attributes'][0]);
+
+ $releaseYearIndex = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'releaseYear',
+ 'type' => 'key',
+ 'attributes' => ['releaseYear'],
+ ]);
+
+ $this->assertEquals(202, $releaseYearIndex['headers']['status-code']);
+ $this->assertEquals('releaseYear', $releaseYearIndex['body']['key']);
+ $this->assertEquals('key', $releaseYearIndex['body']['type']);
+ $this->assertCount(1, $releaseYearIndex['body']['attributes']);
+ $this->assertEquals('releaseYear', $releaseYearIndex['body']['attributes'][0]);
+
+ $releaseWithDate1 = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'releaseYearDated',
+ 'type' => 'key',
+ 'attributes' => ['releaseYear', '$createdAt', '$updatedAt'],
+ ]);
+
+ $this->assertEquals(202, $releaseWithDate1['headers']['status-code']);
+ $this->assertEquals('releaseYearDated', $releaseWithDate1['body']['key']);
+ $this->assertEquals('key', $releaseWithDate1['body']['type']);
+ $this->assertCount(3, $releaseWithDate1['body']['attributes']);
+ $this->assertEquals('releaseYear', $releaseWithDate1['body']['attributes'][0]);
+ $this->assertEquals('$createdAt', $releaseWithDate1['body']['attributes'][1]);
+ $this->assertEquals('$updatedAt', $releaseWithDate1['body']['attributes'][2]);
+
+ $releaseWithDate2 = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'birthDay',
+ 'type' => 'key',
+ 'attributes' => ['birthDay'],
+ ]);
+
+ $this->assertEquals(202, $releaseWithDate2['headers']['status-code']);
+ $this->assertEquals('birthDay', $releaseWithDate2['body']['key']);
+ $this->assertEquals('key', $releaseWithDate2['body']['type']);
+ $this->assertCount(1, $releaseWithDate2['body']['attributes']);
+ $this->assertEquals('birthDay', $releaseWithDate2['body']['attributes'][0]);
+
+ // Failure cases
+ $fulltextReleaseYear = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'releaseYearDated',
+ 'type' => 'fulltext',
+ 'attributes' => ['releaseYear'],
+ ]);
+ $this->assertEquals(400, $fulltextReleaseYear['headers']['status-code']);
+
+ $noAttributes = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'none',
+ 'type' => 'key',
+ 'attributes' => [],
+ ]);
+ $this->assertEquals(400, $noAttributes['headers']['status-code']);
+
+ $duplicates = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'duplicate',
+ 'type' => 'fulltext',
+ 'attributes' => ['releaseYear', 'releaseYear'],
+ ]);
+ $this->assertEquals(400, $duplicates['headers']['status-code']);
+
+ $tooLong = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'tooLong',
+ 'type' => 'key',
+ 'attributes' => ['description', 'tagline'],
+ ]);
+ $this->assertEquals(202, $tooLong['headers']['status-code']);
+
+ $fulltextArray = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'ft',
+ 'type' => 'fulltext',
+ 'attributes' => ['actors'],
+ ]);
+ $this->assertEquals(400, $fulltextArray['headers']['status-code']);
+
+ $actorsArray = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'index-actors',
+ 'type' => 'key',
+ 'attributes' => ['actors'],
+ ]);
+ $this->assertEquals(202, $actorsArray['headers']['status-code']);
+
+ $twoLevelsArray = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'index-ip-actors',
+ 'type' => 'key',
+ 'attributes' => ['releaseYear', 'actors'],
+ 'orders' => ['DESC', 'DESC'],
+ ]);
+ $this->assertEquals(202, $twoLevelsArray['headers']['status-code']);
+
+ $unknown = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'index-unknown',
+ 'type' => 'key',
+ 'attributes' => ['Unknown'],
+ ]);
+ $this->assertEquals(202, $unknown['headers']['status-code']);
+
+ $index1 = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'integers-order',
+ 'type' => 'key',
+ 'attributes' => ['integers'],
+ 'orders' => ['DESC'],
+ ]);
+ $this->assertEquals(202, $index1['headers']['status-code']);
+
+ $index2 = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$moviesId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'integers-size',
+ 'type' => 'key',
+ 'attributes' => ['integers'],
+ ]);
+ $this->assertEquals(202, $index2['headers']['status-code']);
+
+ // Let worker create indexes
+ sleep(2);
+
+ $moviesWithIndexes = $this->client->call('GET', "/documentsdb/{$databaseId}/collections/{$moviesId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertIsArray($moviesWithIndexes['body']['indexes']);
+ $this->assertCount(10, $moviesWithIndexes['body']['indexes']);
+
+ $this->assertEventually(function () use ($databaseId, $moviesId) {
+ $movies = $this->client->call('GET', "/documentsdb/{$databaseId}/collections/{$moviesId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ foreach ($movies['body']['indexes'] as $index) {
+ $this->assertEquals('available', $index['status']);
+ }
+
+ return true;
+ }, 60000, 500);
+ }
+
+ public function testGetIndexByKeyWithLengths(): void
+ {
+ $database = $this->client->call(
+ 'POST',
+ '/documentsdb',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'databaseId' => ID::unique(),
+ 'name' => 'DocumentsDB Index Lengths',
+ ]
+ );
+
+ $this->assertNotEmpty($database['body']['$id']);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $collection = $this->client->call(
+ 'POST',
+ "/documentsdb/{$databaseId}/collections",
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'documentSecurity' => true,
+ ]
+ );
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ $create = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'lengthTestIndex',
+ 'type' => 'key',
+ 'attributes' => ['title', 'description'],
+ 'lengths' => [128, 200],
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+
+ $index = $this->client->call('GET', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes/lengthTestIndex", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $index['headers']['status-code']);
+ $this->assertEquals('lengthTestIndex', $index['body']['key']);
+ $this->assertEquals([128, 200], $index['body']['lengths']);
+
+ $create = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'lengthOverrideTestIndex',
+ 'type' => 'key',
+ 'attributes' => ['actors-new'],
+ 'lengths' => [Database::MAX_ARRAY_INDEX_LENGTH],
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+
+ $index = $this->client->call('GET', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes/lengthOverrideTestIndex", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals([Database::MAX_ARRAY_INDEX_LENGTH], $index['body']['lengths']);
+
+ $create = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'lengthCountExceededIndex',
+ 'type' => 'key',
+ 'attributes' => ['title-not-throw-error'],
+ 'lengths' => [128, 128],
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+
+ $create = $this->client->call('POST', "/documentsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'lengthTooLargeIndex',
+ 'type' => 'key',
+ 'attributes' => ['title', 'description', 'tagline', 'actors'],
+ 'lengths' => [256, 256, 256, 20],
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/DocumentsDBConsoleClientTest.php b/tests/e2e/Services/Databases/DocumentsDBConsoleClientTest.php
new file mode 100644
index 0000000000..895cf67490
--- /dev/null
+++ b/tests/e2e/Services/Databases/DocumentsDBConsoleClientTest.php
@@ -0,0 +1,16 @@
+authorization)) {
+ return $this->authorization;
+ }
+
+ $this->authorization = new Authorization();
+
+ return $this->authorization;
+ }
+
+ public function createCollection(): array
+ {
+ $database = $this->client->call(
+ Client::METHOD_POST,
+ $this->getDatabaseUrl(),
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]),
+ [
+ 'databaseId' => ID::unique(),
+ 'name' => 'InvalidDocumentDatabase',
+ ]
+ );
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('InvalidDocumentDatabase', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $publicMovies = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Movies',
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ );
+ $this->assertEquals(201, $publicMovies['headers']['status-code']);
+
+ $privateMovies = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Movies',
+ 'permissions' => [],
+ $this->getSecurityParam() => true,
+ ]
+ );
+ $this->assertEquals(201, $privateMovies['headers']['status-code']);
+
+ $publicCollection = ['id' => $publicMovies['body']['$id']];
+ $privateCollection = ['id' => $privateMovies['body']['$id']];
+
+ return [
+ 'databaseId' => $databaseId,
+ 'publicCollectionId' => $publicCollection['id'],
+ 'privateCollectionId' => $privateCollection['id'],
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())]],
+ [[Permission::read(Role::users())]],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ ];
+ }
+
+ #[DataProvider('permissionsProvider')]
+ public function testReadDocuments($permissions)
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $publicResponse = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $publicCollectionId),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ 'permissions' => $permissions,
+ ]
+ );
+ $privateResponse = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $privateCollectionId),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ 'permissions' => $permissions,
+ ]
+ );
+
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicDocuments = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($databaseId, $publicCollectionId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]
+ );
+ $privateDocuments = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($databaseId, $privateCollectionId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]
+ );
+
+ $recordKey = $this->getRecordResource();
+ $this->assertEquals(1, $publicDocuments['body']['total']);
+ $this->assertEquals($permissions, $publicDocuments['body'][$recordKey][0]['$permissions']);
+
+ if (\in_array(Permission::read(Role::any()), $permissions)) {
+ $this->assertEquals(1, $privateDocuments['body']['total']);
+ $this->assertEquals($permissions, $privateDocuments['body'][$recordKey][0]['$permissions']);
+ } else {
+ $this->assertEquals(0, $privateDocuments['body']['total']);
+ }
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocument()
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicResponse = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $publicCollectionId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ],
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ ]
+ );
+
+ $publicDocumentId = $publicResponse['body']['$id'];
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+
+ $privateResponse = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $privateCollectionId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ],
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ ]
+ );
+
+ $this->assertEquals(401, $privateResponse['headers']['status-code']);
+
+ // Create a document in private collection with API key so we can test that update and delete are also not allowed
+ $privateResponse = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $privateCollectionId),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ ]
+ );
+
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+ $privateDocumentId = $privateResponse['body']['$id'];
+
+ $publicDocument = $this->client->call(
+ Client::METHOD_PATCH,
+ $this->getRecordUrl($databaseId, $publicCollectionId, $publicDocumentId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ],
+ [
+ 'data' => [
+ 'title' => 'Thor: Ragnarok',
+ ],
+ ]
+ );
+
+ $this->assertEquals(200, $publicDocument['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $publicDocument['body']['title']);
+
+ $privateDocument = $this->client->call(
+ Client::METHOD_PATCH,
+ $this->getRecordUrl($databaseId, $privateCollectionId, $privateDocumentId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ],
+ [
+ 'data' => [
+ 'title' => 'Thor: Ragnarok',
+ ],
+ ]
+ );
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ $publicDocument = $this->client->call(
+ Client::METHOD_DELETE,
+ $this->getRecordUrl($databaseId, $publicCollectionId, $publicDocumentId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]
+ );
+
+ $this->assertEquals(204, $publicDocument['headers']['status-code']);
+
+ $privateDocument = $this->client->call(
+ Client::METHOD_DELETE,
+ $this->getRecordUrl($databaseId, $privateCollectionId, $privateDocumentId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]
+ );
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocumentWithPermissions()
+ {
+ $database = $this->client->call(
+ Client::METHOD_POST,
+ $this->getDatabaseUrl(),
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]),
+ [
+ 'databaseId' => ID::unique(),
+ 'name' => 'GuestPermissionsWrite',
+ ]
+ );
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('GuestPermissionsWrite', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $movies = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Movies',
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ $this->getSecurityParam() => true,
+ ]
+ );
+
+ $moviesId = $movies['body']['$id'];
+
+ $document = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $moviesId),
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ],
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Thor: Ragnarok',
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ ],
+ ]
+ );
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsMemberTest.php b/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsMemberTest.php
new file mode 100644
index 0000000000..88e84b017d
--- /dev/null
+++ b/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsMemberTest.php
@@ -0,0 +1,238 @@
+ $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())], 1, 1, 1],
+ [[Permission::read(Role::users())], 1, 1, 1],
+ [[Permission::read(Role::user(ID::custom('random')))], 1, 1, 0],
+ [[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], 1, 1, 0],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 1, 1, 0],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 1, 1, 0],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())], 1, 1, 0],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], 1, 1, 1],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], 1, 1, 1],
+ [[Permission::read(Role::user(ID::custom('user1')))], 1, 1, 1],
+ [[Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], 1, 1, 1],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())], 1, 1, 1],
+ ];
+ }
+
+ /**
+ * Setup database helper
+ */
+ protected function setupDatabase(): array
+ {
+ $cacheKey = $this->getProject()['$id'] . '_' . static::class;
+
+ if (!empty(self::$setupDatabaseCache[$cacheKey])) {
+ return self::$setupDatabaseCache[$cacheKey];
+ }
+
+ $this->createUsers();
+
+ $db = $this->client->call(
+ Client::METHOD_POST,
+ $this->getDatabaseUrl(),
+ $this->getServerHeader(),
+ [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database',
+ ]
+ );
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $databaseId = $db['body']['$id'];
+
+ $public = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Movies',
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ $this->getSecurityParam() => true,
+ ]
+ );
+ $this->assertEquals(201, $public['headers']['status-code']);
+ $this->collections = ['public' => $public['body']['$id']];
+
+ $private = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Private Movies',
+ 'permissions' => [
+ Permission::read(Role::users()),
+ Permission::create(Role::users()),
+ Permission::update(Role::users()),
+ Permission::delete(Role::users()),
+ ],
+ $this->getSecurityParam() => true,
+ ]
+ );
+ $this->assertEquals(201, $private['headers']['status-code']);
+ $this->collections['private'] = $private['body']['$id'];
+
+ $doconly = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::unique(),
+ 'name' => 'Document Only Movies',
+ 'permissions' => [],
+ $this->getSecurityParam() => true,
+ ]
+ );
+ $this->assertEquals(201, $doconly['headers']['status-code']);
+ $this->collections['doconly'] = $doconly['body']['$id'];
+
+ self::$setupDatabaseCache[$cacheKey] = [
+ 'users' => $this->users,
+ 'collections' => $this->collections,
+ 'databaseId' => $databaseId,
+ ];
+
+ return self::$setupDatabaseCache[$cacheKey];
+ }
+
+ #[DataProvider('permissionsProvider')]
+ public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount)
+ {
+ $data = $this->setupDatabase();
+ $users = $data['users'];
+ $collections = $data['collections'];
+ $databaseId = $data['databaseId'];
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $collections['public']),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ 'permissions' => $permissions,
+ ]
+ );
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $collections['private']),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ 'permissions' => $permissions,
+ ]
+ );
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($databaseId, $collections['doconly']),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ 'permissions' => $permissions,
+ ]
+ );
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ /**
+ * Check "any" permission collection
+ */
+ $documents = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($databaseId, $collections['public']),
+ [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]
+ );
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertGreaterThanOrEqual($anyCount, $documents['body']['total']);
+
+ /**
+ * Check "users" permission collection
+ */
+ $documents = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($databaseId, $collections['private']),
+ [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]
+ );
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertGreaterThanOrEqual($usersCount, $documents['body']['total']);
+
+ /**
+ * Check "user:user1" document only permission collection
+ */
+ $documents = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($databaseId, $collections['doconly']),
+ [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]
+ );
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertGreaterThanOrEqual($docOnlyCount, $documents['body']['total']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsTeamTest.php b/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsTeamTest.php
new file mode 100644
index 0000000000..db9adf9bb1
--- /dev/null
+++ b/tests/e2e/Services/Databases/Permissions/DocumentsDBPermissionsTeamTest.php
@@ -0,0 +1,234 @@
+ $this->createTeam('team1', 'Team 1'),
+ 'team2' => $this->createTeam('team2', 'Team 2'),
+ ];
+ }
+
+ public function createUsers(): array
+ {
+ return [
+ 'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ 'user3' => $this->createUser('user3', 'sit@ipsum.com'),
+ ];
+ }
+
+ public function createCollections($teams)
+ {
+ $db = $this->client->call(
+ Client::METHOD_POST,
+ $this->getDatabaseUrl(),
+ $this->getServerHeader(),
+ [
+ 'databaseId' => $this->databaseId,
+ 'name' => 'Test Database',
+ ]
+ );
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $collection1 = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($this->databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::custom('collection1'),
+ 'name' => 'Collection 1',
+ 'permissions' => [
+ Permission::read(Role::team($teams['team1']['$id'])),
+ Permission::create(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::update(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
+ ],
+ ]
+ );
+ $this->assertEquals(201, $collection1['headers']['status-code']);
+
+ $this->collections['collection1'] = $collection1['body']['$id'];
+
+ $collection2 = $this->client->call(
+ Client::METHOD_POST,
+ $this->getContainerUrl($this->databaseId),
+ $this->getServerHeader(),
+ [
+ $this->getContainerIdParam() => ID::custom('collection2'),
+ 'name' => 'Collection 2',
+ 'permissions' => [
+ Permission::read(Role::team($teams['team2']['$id'])),
+ Permission::create(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::update(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
+ ],
+ ]
+ );
+ $this->assertEquals(201, $collection2['headers']['status-code']);
+
+ $this->collections['collection2'] = $collection2['body']['$id'];
+
+ return $this->collections;
+ }
+
+ /*
+ * $success = can $user read from $collection
+ * [$user, $collection, $success]
+ */
+ public static function readDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', true],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', true],
+ ];
+ }
+
+ /*
+ * $success = can $user write to $collection
+ * [$user, $collection, $success]
+ */
+ public static function writeDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', false],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', false],
+ ];
+ }
+
+ /**
+ * Setup database helper
+ */
+ protected function setupDatabase(): array
+ {
+ $cacheKey = $this->getProject()['$id'] . '_' . static::class;
+
+ if (!empty(self::$setupDatabaseCache[$cacheKey])) {
+ return self::$setupDatabaseCache[$cacheKey];
+ }
+
+ $this->createUsers();
+ $this->createTeams();
+
+ $this->addToTeam('user1', 'team1', ['admin']);
+ $this->addToTeam('user2', 'team2', ['owner']);
+
+ // user3 in both teams but with no roles
+ $this->addToTeam('user3', 'team1');
+ $this->addToTeam('user3', 'team2');
+
+ $this->createCollections($this->teams);
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($this->databaseId, $this->collections['collection1']),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Lorem',
+ ],
+ ]
+ );
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($this->databaseId, $this->collections['collection2']),
+ $this->getServerHeader(),
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Ipsum',
+ ],
+ ]
+ );
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ self::$setupDatabaseCache[$cacheKey] = $this->users;
+
+ return self::$setupDatabaseCache[$cacheKey];
+ }
+
+ #[DataProvider('readDocumentsProvider')]
+ public function testReadDocuments($user, $collection, $success)
+ {
+ $users = $this->setupDatabase();
+
+ $documents = $this->client->call(
+ Client::METHOD_GET,
+ $this->getRecordUrl($this->databaseId, $collection),
+ [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ]
+ );
+
+ if ($success) {
+ $this->assertCount(1, $documents['body'][$this->getRecordResource()]);
+ } else {
+ $this->assertEquals(401, $documents['headers']['status-code']);
+ }
+ }
+
+ #[DataProvider('writeDocumentsProvider')]
+ public function testWriteDocuments($user, $collection, $success)
+ {
+ $users = $this->setupDatabase();
+
+ $documents = $this->client->call(
+ Client::METHOD_POST,
+ $this->getRecordUrl($this->databaseId, $collection),
+ [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ],
+ [
+ $this->getRecordIdParam() => ID::unique(),
+ 'data' => [
+ 'title' => 'Ipsum',
+ ],
+ ]
+ );
+
+ if ($success) {
+ $this->assertEquals(201, $documents['headers']['status-code']);
+ } else {
+ // 401 if user is a part of team, 404 otherwise
+ $this->assertContains($documents['headers']['status-code'], [401, 404]);
+ }
+ }
+}
diff --git a/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsGuestTest.php b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsGuestTest.php
new file mode 100644
index 0000000000..52ddcc8586
--- /dev/null
+++ b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsGuestTest.php
@@ -0,0 +1,281 @@
+authorization)) {
+ return $this->authorization;
+ }
+
+ $this->authorization = new Authorization();
+
+ return $this->authorization;
+ }
+
+ public function createCollection(): array
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'VectorGuestDB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('VectorGuestDB', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $publicMovies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+ $privateMovies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+
+ $publicCollection = ['id' => $publicMovies['body']['$id']];
+ $privateCollection = ['id' => $privateMovies['body']['$id']];
+
+ return [
+ 'databaseId' => $databaseId,
+ 'publicCollectionId' => $publicCollection['id'],
+ 'privateCollectionId' => $privateCollection['id'],
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())]],
+ [[Permission::read(Role::users())]],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ ];
+ }
+
+ #[DataProvider('permissionsProvider')]
+ public function testReadDocuments($permissions)
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $publicResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions,
+ ]);
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions,
+ ]);
+
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicDocuments = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+ $privateDocuments = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(1, $publicDocuments['body']['total']);
+ $this->assertEquals($permissions, $publicDocuments['body']['documents'][0]['$permissions']);
+
+ if (\in_array(Permission::read(Role::any()), $permissions)) {
+ $this->assertEquals(1, $privateDocuments['body']['total']);
+ $this->assertEquals($permissions, $privateDocuments['body']['documents'][0]['$permissions']);
+ } else {
+ $this->assertEquals(0, $privateDocuments['body']['total']);
+ }
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocument()
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ]
+ ]);
+
+ $publicDocumentId = $publicResponse['body']['$id'];
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+
+ $this->assertEquals(401, $privateResponse['headers']['status-code']);
+
+ // Create a document in private collection with API key so we can test that update and delete are also not allowed
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 0.0, 1.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+ $privateDocumentId = $privateResponse['body']['$id'];
+
+ $publicDocument = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'data' => [
+ 'embeddings' => [0.5, 0.5, 0.0],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ ]);
+
+ $this->assertEquals(200, $publicDocument['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $publicDocument['body']['metadata']['title']);
+
+ $privateDocument = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'data' => [
+ 'embeddings' => [0.2, 0.3, 0.5],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ ]);
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ $publicDocument = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(204, $publicDocument['headers']['status-code']);
+
+ $privateDocument = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocumentWithPermissions()
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'VectorGuestPermsWrite',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('VectorGuestPermsWrite', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ 'documentSecurity' => true
+ ]);
+
+ $moviesId = $movies['body']['$id'];
+
+ $document = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ ]
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $document['body']['metadata']['title']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsMemberTest.php b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsMemberTest.php
new file mode 100644
index 0000000000..3043a42dd5
--- /dev/null
+++ b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsMemberTest.php
@@ -0,0 +1,197 @@
+ $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())], 1, 1, 1],
+ [[Permission::read(Role::users())], 2, 2, 2],
+ [[Permission::read(Role::user(ID::custom('random')))], 3, 3, 2],
+ [[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], 4, 4, 2],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 5, 5, 2],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 6, 6, 2],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())], 7, 7, 2],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], 8, 8, 3],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], 9, 9, 4],
+ [[Permission::read(Role::user(ID::custom('user1')))], 10, 10, 5],
+ [[Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], 11, 11, 6],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())], 12, 12, 7],
+ ];
+ }
+
+ /**
+ * Setup database
+ *
+ * Data providers lose object state so explicitly pass [$users, $collections] to each iteration
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function testSetupDatabase(): array
+ {
+ $this->createUsers();
+
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', $this->getServerHeader(), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $databaseId = $db['body']['$id'];
+
+ $public = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $public['headers']['status-code']);
+ $this->collections = ['public' => $public['body']['$id']];
+
+ $private = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Private Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::users()),
+ Permission::create(Role::users()),
+ Permission::update(Role::users()),
+ Permission::delete(Role::users()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $private['headers']['status-code']);
+ $this->collections['private'] = $private['body']['$id'];
+
+ $doconly = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Document Only Movies',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $private['headers']['status-code']);
+ $this->collections['doconly'] = $doconly['body']['$id'];
+
+ return [
+ 'users' => $this->users,
+ 'collections' => $this->collections,
+ 'databaseId' => $databaseId
+ ];
+ }
+
+ /**
+ * Data provider params are passed before test dependencies.
+ */
+ #[DataProvider('permissionsProvider')]
+ #[Depends('testSetupDatabase')]
+ public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
+ {
+ $users = $data['users'];
+ $collections = $data['collections'];
+ $databaseId = $data['databaseId'];
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 0.0, 1.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ /**
+ * Check "any" permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($anyCount, $documents['body']['total']);
+
+ /**
+ * Check "users" permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($usersCount, $documents['body']['total']);
+
+ /**
+ * Check "user:user1" document only permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($docOnlyCount, $documents['body']['total']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsTeamTest.php b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsTeamTest.php
new file mode 100644
index 0000000000..11709ed729
--- /dev/null
+++ b/tests/e2e/Services/Databases/Permissions/VectorsDBPermissionsTeamTest.php
@@ -0,0 +1,200 @@
+ $this->createTeam('team1', 'Team 1'),
+ 'team2' => $this->createTeam('team2', 'Team 2'),
+ ];
+ }
+
+ public function createUsers(): array
+ {
+ return [
+ 'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ 'user3' => $this->createUser('user3', 'sit@ipsum.com'),
+ ];
+ }
+
+ public function createCollections($teams)
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', $this->getServerHeader(), [
+ 'databaseId' => $this->databaseId,
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $collection1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::custom('collection1'),
+ 'name' => 'Collection 1',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::team($teams['team1']['$id'])),
+ Permission::create(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::update(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
+ ],
+ ]);
+
+ $this->collections['collection1'] = $collection1['body']['$id'];
+
+ $collection2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::custom('collection2'),
+ 'name' => 'Collection 2',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::team($teams['team2']['$id'])),
+ Permission::create(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::update(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
+ ]
+ ]);
+
+ $this->collections['collection2'] = $collection2['body']['$id'];
+
+ return $this->collections;
+ }
+
+ /*
+ * $success = can $user read from $collection
+ * [$user, $collection, $success]
+ */
+ public static function readDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', true],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', true],
+ ];
+ }
+
+ /*
+ * $success = can $user write to $collection
+ * [$user, $collection, $success]
+ */
+ public static function writeDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', false],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', false],
+ ];
+ }
+
+ /**
+ * Setup database
+ *
+ * Data providers lose object state
+ * so explicitly pass $users to each iteration
+ * @return array $users
+ */
+ public function testSetupDatabase(): array
+ {
+ $this->createUsers();
+ $this->createTeams();
+
+ $this->addToTeam('user1', 'team1', ['admin']);
+ $this->addToTeam('user2', 'team2', ['owner']);
+
+ // user3 in both teams but with no roles
+ $this->addToTeam('user3', 'team1');
+ $this->addToTeam('user3', 'team2');
+
+ $this->createCollections($this->teams);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Ipsum'],
+ ],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ return $this->users;
+ }
+
+ /**
+ * Data provider params are passed before test dependencies.
+ */
+ #[Depends('testSetupDatabase')]
+ #[DataProvider('readDocumentsProvider')]
+ public function testReadDocuments($user, $collection, $success, $users)
+ {
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ]);
+
+ if ($success) {
+ $this->assertCount(1, $documents['body']['documents']);
+ } else {
+ $this->assertEquals(401, $documents['headers']['status-code']);
+ }
+ }
+
+ #[Depends('testSetupDatabase')]
+ #[DataProvider('writeDocumentsProvider')]
+ public function testWriteDocuments($user, $collection, $success, $users)
+ {
+ $documents = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.2, 0.3, 0.5],
+ 'metadata' => ['title' => 'Ipsum'],
+ ],
+ ]);
+
+ if ($success) {
+ $this->assertEquals(201, $documents['headers']['status-code']);
+ } else {
+ // 401 if user is a part of team, 404 otherwise
+ $this->assertContains($documents['headers']['status-code'], [401, 404]);
+ }
+ }
+}
diff --git a/tests/e2e/Services/Databases/Transactions/ACIDBase.php b/tests/e2e/Services/Databases/Transactions/ACIDBase.php
index 070b83734f..1a6ee83b33 100644
--- a/tests/e2e/Services/Databases/Transactions/ACIDBase.php
+++ b/tests/e2e/Services/Databases/Transactions/ACIDBase.php
@@ -47,18 +47,20 @@ trait ACIDBase
$collectionId = $collection['body']['$id'];
- // Add unique attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add unique attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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,
+ ]);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Add unique index
$this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
@@ -174,6 +176,11 @@ trait ACIDBase
*/
public function testConsistency(): void
{
+ if (!$this->getSupportForAttributes()) {
+ $this->markTestSkipped('This adapter does not support attributes; schema constraint consistency cannot be tested.');
+ return;
+ }
+
// Create database
$database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
'content-type' => 'application/json',
@@ -336,19 +343,21 @@ trait ACIDBase
$collectionId = $collection['body']['$id'];
- // Add counter attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add counter attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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
+ ]);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Create initial document with counter
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
@@ -494,18 +503,20 @@ trait ACIDBase
$collectionId = $collection['body']['$id'];
- // Add attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, '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,
+ ]);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Create and commit transaction with multiple operations
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
diff --git a/tests/e2e/Services/Databases/Transactions/DocumentsDBACIDTest.php b/tests/e2e/Services/Databases/Transactions/DocumentsDBACIDTest.php
new file mode 100644
index 0000000000..eb597e3488
--- /dev/null
+++ b/tests/e2e/Services/Databases/Transactions/DocumentsDBACIDTest.php
@@ -0,0 +1,18 @@
+assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -150,18 +152,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create a document first with API key
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
@@ -224,18 +228,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
'content-type' => 'application/json',
@@ -297,18 +303,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create a document with update permission at document level
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
@@ -376,18 +384,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create a document with delete permission at document level
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($this->getPermissionsDatabase(), $collection['body']['$id']), array_merge([
@@ -457,18 +467,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
// Add attribute
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -527,18 +539,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -597,18 +611,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -660,18 +676,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -723,18 +741,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -1060,18 +1080,20 @@ trait TransactionPermissionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($this->getPermissionsDatabase(), $collection['body']['$id'], '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']);
- $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($this->getPermissionsDatabase(), $collection['body']['$id']);
+ }
// Create user 1 (fresh) and their transaction
$user1 = $this->getUser(true);
diff --git a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php
index 479e4d5c68..c69c35f663 100644
--- a/tests/e2e/Services/Databases/Transactions/TransactionsBase.php
+++ b/tests/e2e/Services/Databases/Transactions/TransactionsBase.php
@@ -70,19 +70,21 @@ trait TransactionsBase
$this->assertEquals(201, $collection['headers']['status-code']);
self::$sharedCollectionId = $collection['body']['$id'];
- // Create a standard 'name' attribute
- $nameAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, self::$sharedCollectionId, "string", null), 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, $nameAttr['headers']['status-code']);
+ // Create a standard 'name' attribute only if attributes are supported
+ if ($this->getSupportForAttributes()) {
+ $nameAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, self::$sharedCollectionId, "string", null), 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, $nameAttr['headers']['status-code']);
- $this->waitForAllAttributes($databaseId, self::$sharedCollectionId);
+ $this->waitForAllAttributes($databaseId, self::$sharedCollectionId);
+ }
return self::$sharedCollectionId;
}
@@ -219,20 +221,22 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attributes
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), 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']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
- // Wait for attribute to be created
- $this->waitForAllAttributes($databaseId, $collectionId);
+ // Wait for attribute to be created
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Add valid operations
$response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
@@ -365,18 +369,20 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attributes
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), 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']);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -517,17 +523,19 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'value',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'value',
+ 'size' => 256,
+ 'required' => true,
+ ]);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Add operations
$response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
@@ -607,17 +615,18 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'data',
- 'size' => 256,
- 'required' => false,
- ]);
-
- $this->waitForAllAttributes($databaseId, $collectionId);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'data',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Create transaction with minimum TTL (60 seconds)
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -697,17 +706,19 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'value',
- 'size' => 256,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'value',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -819,19 +830,21 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attribute
- $counterAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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,
- ]);
- $this->assertEquals(202, $counterAttr['headers']['status-code']);
+ if ($this->getSupportForAttributes()) {
+ $counterAttr = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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,
+ ]);
+ $this->assertEquals(202, $counterAttr['headers']['status-code']);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create initial document
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -959,17 +972,19 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'data',
- 'size' => 256,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'data',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create document
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -1064,27 +1079,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create some initial documents
for ($i = 1; $i <= 5; $i++) {
@@ -1228,17 +1245,19 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes with constraints
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'email',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'email',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create unique index on email
$this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId, null), array_merge([
@@ -1361,18 +1380,20 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
- // Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'data',
- 'size' => 256,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Create attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'data',
+ 'size' => 256,
+ 'required' => false,
+ ]);
- $this->waitForAllAttributes($databaseId, $collectionId);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Test double commit
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -1485,18 +1506,21 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
- // Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'data',
- 'size' => 256,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Create attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'data',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -1595,20 +1619,22 @@ trait TransactionsBase
['key' => 'data', 'type' => 'string', 'size' => 256, 'required' => false],
];
- foreach ($attributes as $attr) {
- $type = $attr['type'];
- unset($attr['type']);
+ if ($this->getSupportForAttributes()) {
+ foreach ($attributes as $attr) {
+ $type = $attr['type'];
+ unset($attr['type']);
- $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, $type, null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), $attr);
+ $response = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, $type, null), 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']);
+ $this->assertEquals(202, $response['headers']['status-code']);
+ }
+ $this->waitForAllAttributes($databaseId, $collectionId);
}
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -1699,38 +1725,40 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'category',
- 'size' => 256,
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'category',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create document outside transaction
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -1836,28 +1864,30 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -2031,27 +2061,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'category',
- 'size' => 256,
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'category',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -2175,27 +2207,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'category',
- 'size' => 256,
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'category',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create documents for bulk testing
for ($i = 1; $i <= 3; $i++) {
@@ -2299,28 +2333,30 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create one document outside transaction
$this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -2445,27 +2481,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'category',
- 'size' => 256,
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'category',
+ 'size' => 256,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create documents for bulk testing
for ($i = 1; $i <= 3; $i++) {
@@ -2569,38 +2607,40 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create an existing document outside transaction for testing
$existingDoc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -2848,18 +2888,21 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
- // Create attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Create attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -2990,36 +3033,38 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create some existing documents
for ($i = 1; $i <= 3; $i++) {
@@ -3170,37 +3215,39 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'priority',
- 'size' => 256,
- 'required' => true,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'priority',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create existing documents
for ($i = 1; $i <= 4; $i++) {
@@ -3345,27 +3392,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'type',
- 'size' => 256,
- 'required' => true,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'type',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create existing documents
for ($i = 1; $i <= 3; $i++) {
@@ -3507,27 +3556,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Create attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create existing documents
for ($i = 1; $i <= 5; $i++) {
@@ -3663,28 +3714,31 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
- // Add integer attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'counter',
- 'required' => false,
- 'default' => 0,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add integer attributes
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'score',
- 'required' => false,
- 'default' => 100,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'score',
+ 'required' => false,
+ 'default' => 100,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create initial document
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -3822,18 +3876,21 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
- // Add balance attribute
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'balance',
- 'required' => false,
- 'default' => 0,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add balance attribute
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'balance',
+ 'required' => false,
+ 'default' => 0,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create initial documents
$this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -3965,27 +4022,29 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'status',
- 'size' => 50,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'category',
- 'size' => 50,
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'category',
+ 'size' => 50,
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create initial documents
for ($i = 1; $i <= 5; $i++) {
@@ -4107,26 +4166,28 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 100,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'value',
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'value',
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create some initial documents
$this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId, null), array_merge([
@@ -4266,26 +4327,28 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add attributes
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'type',
- 'size' => 50,
- 'required' => false,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "string", null), 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, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'priority',
- 'required' => false,
- ]);
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, "integer", null), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'priority',
+ 'required' => false,
+ ]);
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
- $this->waitForAllAttributes($databaseId, $collectionId);
// Create initial documents
for ($i = 1; $i <= 10; $i++) {
@@ -4405,20 +4468,22 @@ trait TransactionsBase
$collectionId = $collection['body']['$id'];
// Add required attribute
- $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $attribute = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string', null), 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']);
+ $this->assertEquals(202, $attribute['headers']['status-code']);
- // Wait for attribute to be ready
- $this->waitForAllAttributes($databaseId, $collectionId);
+ // Wait for attribute to be ready
+ $this->waitForAllAttributes($databaseId, $collectionId);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -4852,27 +4917,29 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
// Add columns
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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, $this->getSchemaUrl($databaseId, $tableId, '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, $this->getSchemaUrl($databaseId, $tableId, '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->waitForAllAttributes($databaseId, $tableId);
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
// Create initial row
$row = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tableId), array_merge([
@@ -4987,18 +5054,21 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
- // Add balance column
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, 'integer'), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'balance',
- 'required' => false,
- 'default' => 0,
- ]);
+ if ($this->getSupportForAttributes()) {
+ // Add balance column
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, 'integer'), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'balance',
+ 'required' => false,
+ 'default' => 0,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
- $this->waitForAllAttributes($databaseId, $tableId);
// Create initial row
$this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tableId), array_merge([
@@ -5095,17 +5165,19 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
// Add columns
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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->waitForAllAttributes($databaseId, $tableId);
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -5205,17 +5277,20 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
- $this->waitForAllAttributes($databaseId, $tableId);
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
@@ -5309,17 +5384,20 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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->waitForAllAttributes($databaseId, $tableId);
+ }
- $this->waitForAllAttributes($databaseId, $tableId);
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
@@ -5427,17 +5505,20 @@ trait TransactionsBase
'required' => true,
]);
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
- $this->waitForAllAttributes($databaseId, $tableId);
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
@@ -5543,28 +5624,30 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
// Create columns
- $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, '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, $this->getSchemaUrl($databaseId, $tableId, '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, $this->getSchemaUrl($databaseId, $tableId, '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->waitForAllAttributes($databaseId, $tableId);
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
@@ -5687,19 +5770,21 @@ trait TransactionsBase
$tableId = $table['body']['$id'];
// Create array column
- $column = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, 'string'), array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'key' => 'items',
- 'size' => 255,
- 'required' => false,
- 'array' => true,
- ]);
+ if ($this->getSupportForAttributes()) {
+ $column = $this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $tableId, 'string'), array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'items',
+ 'size' => 255,
+ 'required' => false,
+ 'array' => true,
+ ]);
- $this->assertEquals(202, $column['headers']['status-code']);
- $this->waitForAllAttributes($databaseId, $tableId);
+ $this->assertEquals(202, $column['headers']['status-code']);
+ $this->waitForAllAttributes($databaseId, $tableId);
+ }
// Create initial row with some items
$row = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $tableId), array_merge([
diff --git a/tests/e2e/Services/Databases/Transactions/VectorsDBACIDTest.php b/tests/e2e/Services/Databases/Transactions/VectorsDBACIDTest.php
new file mode 100644
index 0000000000..914c8d0c5b
--- /dev/null
+++ b/tests/e2e/Services/Databases/Transactions/VectorsDBACIDTest.php
@@ -0,0 +1,528 @@
+client->call(Client::METHOD_POST, '/vectorsdb', 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 for the test
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ // Create a document outside the transaction
+ $existingDocumentId = 'existing_doc';
+ $doc1 = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => $existingDocumentId,
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['email' => 'existing@example.com'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $doc1['headers']['status-code']);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/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 create reuses an existing documentId and should cause the commit to fail
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => 'txn_doc_1',
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['email' => 'newuser@example.com'],
+ ],
+ [
+ '$id' => $existingDocumentId,
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['email' => 'duplicate@example.com'],
+ ],
+ [
+ '$id' => 'txn_doc_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.4),
+ 'metadata' => ['email' => 'should-not-exist@example.com'],
+ ],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code'], 'Adding documents via normal route should succeed. Response: ' . json_encode($response['body']));
+
+ // Attempt to commit - should fail due to duplicate document ID
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Verify NO new documents were created (atomicity)
+ $documents = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(1, $documents['body']['total']);
+ $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['metadata']['email']);
+ }
+
+ /**
+ * Test consistency - schema validation and constraints
+ */
+ public function testConsistency(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Stage operations with valid and invalid data (embedding length mismatch)
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['name' => 'Valid User'],
+ ],
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(2, 0.5), // Invalid dimensions
+ 'metadata' => ['name' => 'Invalid User'],
+ ],
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.6),
+ 'metadata' => ['name' => 'Should Not Persist'],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Attempt to commit - should fail due to invalid embeddings
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, 409, 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, "/vectorsdb/{$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 testIsolation(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create initial document with status metadata
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => 'shared_doc',
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['status' => 'pending'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create first transaction
+ $transaction1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/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'];
+
+ // Transaction 1: update status to approved
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['status' => 'approved'],
+ ],
+ ],
+ ],
+ ]);
+
+ // Commit first transaction
+ $response1 = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Document should reflect the first transaction's update
+ $document = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('approved', $document['body']['metadata']['status']);
+
+ // Create second transaction after first commit
+ $transaction2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/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 2: update status to declined
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['status' => 'declined'],
+ ],
+ ],
+ ],
+ ]);
+
+ // Commit second transaction and ensure isolation guarantees
+ $response2 = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Final document should reflect the second transaction's update
+ $document = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals('declined', $document['body']['metadata']['status']);
+ }
+
+ /**
+ * Test durability - committed data persists
+ */
+ public function testDurability(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ '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'];
+
+ // Create transaction with multiple operations
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/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'];
+
+ // Create two documents via normal route inside transaction
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => 'durable_doc_1',
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['data' => 'Important data 1'],
+ ],
+ [
+ '$id' => 'durable_doc_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.5),
+ 'metadata' => ['data' => 'Important data 2'],
+ ],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ // Update first document inside the same transaction
+ $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'data' => [
+ 'metadata' => ['data' => 'Updated important data 1'],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ // Commit transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Verify documents exist and have correct data
+ $document1 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ $document2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ // Further update outside transaction to ensure persistence
+ $update = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'data' => [
+ 'metadata' => ['data' => 'Modified outside transaction'],
+ ],
+ ]);
+ $this->assertEquals(200, $update['headers']['status-code']);
+
+ // Verify the update persisted
+ $document1 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ // List all documents to verify total count
+ $documents = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", 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/Transactions/VectorsDBTransactionsConsoleClientTest.php b/tests/e2e/Services/Databases/Transactions/VectorsDBTransactionsConsoleClientTest.php
new file mode 100644
index 0000000000..f6f217ab69
--- /dev/null
+++ b/tests/e2e/Services/Databases/Transactions/VectorsDBTransactionsConsoleClientTest.php
@@ -0,0 +1,15 @@
+client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database'
+ ]);
+
+ $this->assertNotEmpty($database['body']['$id']);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Test Database', $database['body']['name']);
+ $this->assertEquals('vectorsdb', $database['body']['type']);
+
+ return ['databaseId' => $database['body']['$id']];
+ }
+
+ #[Depends('testCreateCollectionSample')]
+ public function testCreateDocument(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Build embedding vector matching collection dimensions (1536)
+ $vector = array_fill(0, 1536, 0.1);
+ $vector[0] = 1.0;
+
+ $res = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $vector,
+ 'metadata' => ['type' => 'sample', 'rank' => 1]
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ]
+ ]);
+
+ $this->assertEquals(201, $res['headers']['status-code']);
+ $this->assertNotEmpty($res['body']['$id']);
+ $documentId = $res['body']['$id'];
+
+ // createdAt/updatedAt should be present and equal on initial create
+ $this->assertArrayHasKey('$createdAt', $res['body']);
+ $this->assertArrayHasKey('$updatedAt', $res['body']);
+ $this->assertNotEmpty($res['body']['$createdAt']);
+ $this->assertNotEmpty($res['body']['$updatedAt']);
+ $this->assertEquals($res['body']['$createdAt'], $res['body']['$updatedAt']);
+
+ // Edge: invalid dimensions (vector too short) → expect 4xx
+ $badVec = [1.0, 0.0];
+ $bad = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $badVec,
+ 'metadata' => ['type' => 'bad']
+ ],
+ ]);
+ $this->assertGreaterThanOrEqual(400, $bad['headers']['status-code']);
+ $this->assertLessThan(500, $bad['headers']['status-code']);
+
+ // Edge: invalid type values (strings) → expect 4xx
+ $strVec = ['1.0', '0.0', '0.0'];
+ $bad2 = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $strVec,
+ 'metadata' => ['type' => 'bad-strings']
+ ],
+ ]);
+ $this->assertGreaterThanOrEqual(400, $bad2['headers']['status-code']);
+ $this->assertLessThan(500, $bad2['headers']['status-code']);
+
+ // Create another valid doc to verify list totals later
+ $res2 = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $vector,
+ 'metadata' => ['type' => 'sample', 'rank' => 99]
+ ],
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $res2['headers']['status-code']);
+ $documentId2 = $res2['body']['$id'];
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'documentId' => $documentId,
+ 'documentId2' => $documentId2,
+ 'createdAt' => $res['body']['$createdAt'],
+ 'updatedAt' => $res['body']['$updatedAt'],
+ ];
+ }
+
+ #[Depends('testCreateDocument')]
+ public function testGetDocument(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+ $documentId = $data['documentId'];
+
+ $res = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals($documentId, $res['body']['$id']);
+
+ // Edge: missing document should return 404
+ $missing = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/" . ID::unique(), [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $missing['headers']['status-code']);
+
+ return $data;
+ }
+
+ #[Depends('testCreateDocument')]
+ public function testListDocuments(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [Query::limit(5)->toString()]
+ ]);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsInt($list['body']['total']);
+ $this->assertGreaterThanOrEqual(1, $list['body']['total']);
+
+ // Pagination: limit 1, then offset 1
+ $page1 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::orderAsc('$id')->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $page1['headers']['status-code']);
+ $this->assertEquals(1, \count($page1['body']['documents'] ?? []));
+
+ $page2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::offset(1)->toString(),
+ Query::orderAsc('$id')->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $page2['headers']['status-code']);
+ $this->assertEquals(1, \count($page2['body']['documents'] ?? []));
+
+ return $data;
+ }
+
+ #[Depends('testCreateDocument')]
+ public function testUpsertDocument(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+ $documentId = $data['documentId'];
+
+ $vector = array_fill(0, 1536, 0.0);
+ // $vector[1] = 1.0;
+
+ $upd = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [
+ 'embeddings' => $vector,
+ 'metadata' => ['type' => 'sample', 'rank' => 2]
+ ]
+ ]);
+
+ $this->assertEquals(200, $upd['headers']['status-code']);
+
+ // Verify update took effect
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals(2, $get['body']['metadata']['rank']);
+ // updatedAt should be greater or changed from earlier
+ $this->assertArrayHasKey('$updatedAt', $get['body']);
+
+ return $data;
+ }
+
+ #[Depends('testUpsertDocument')]
+ public function testUpdateDocument(array $data): array
+ {
+ // Upsert is used for update semantics
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+ $documentId = $data['documentId'];
+
+ $vector = array_fill(0, 1536, 0.0);
+ $vector[2] = 1.0;
+
+ $upd = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [
+ 'embeddings' => $vector,
+ 'metadata' => ['type' => 'sample', 'rank' => 3]
+ ]
+ ]);
+
+ $this->assertEquals(200, $upd['headers']['status-code']);
+
+ // Re-update to check idempotence and metadata replacement
+ $vector2 = array_fill(0, 1536, 0.0);
+ $vector2[3] = 1.0;
+ $upd2 = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [
+ 'embeddings' => $vector2,
+ 'metadata' => ['type' => 'sample', 'rank' => 4]
+ ]
+ ]);
+ $this->assertEquals(200, $upd2['headers']['status-code']);
+
+ // Verify updatedAt changed again
+ $get2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get2['headers']['status-code']);
+ $this->assertArrayHasKey('$updatedAt', $get2['body']);
+
+ return $data;
+ }
+
+ #[Depends('testUpdateDocument')]
+ public function testDocumentsVectorQueries(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create two more documents with distinct embeddings
+ $mk = function (array $vec, string $name) use ($databaseId, $collectionId) {
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $vec,
+ 'metadata' => ['name' => $name]
+ ],
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ };
+
+ $vA = array_fill(0, 1536, 0.0);
+ $vA[0] = 1.0; // close to [1,0,0,...]
+ $vB = array_fill(0, 1536, 0.0);
+ $vB[1] = 1.0; // close to [0,1,0,...]
+
+ $mk($vA, 'A');
+ $mk($vB, 'B');
+
+ // Dot product
+ $dot = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorDot('embeddings', $vA)->toString(),
+ Query::limit(2)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $dot['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $dot['body']['total']);
+
+ // Cosine
+ $cos = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorCosine('embeddings', $vB)->toString(),
+ Query::limit(2)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $cos['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $cos['body']['total']);
+
+ // Euclidean
+ $eu = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorEuclidean('embeddings', $vA)->toString(),
+ Query::limit(2)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $eu['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $eu['body']['total']);
+
+ // Combined vector + metadata filters
+ $combo = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorCosine('embeddings', $vA)->toString(),
+ Query::notEqual('metadata', [['name' => 'B']])->toString(),
+ Query::limit(2)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $combo['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $combo['body']['total']);
+
+ // Ordering with $id ascending combined with vector
+ $ordered = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorDot('embeddings', $vA)->toString(),
+ Query::orderAsc('$id')->toString(),
+ Query::limit(3)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $ordered['headers']['status-code']);
+
+ return $data;
+ }
+
+ #[Depends('testDocumentsVectorQueries')]
+ public function testDeleteDocument(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+ $documentId = $data['documentId'];
+
+ $del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+
+ // GET after delete should be 404
+ $getMissing = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $getMissing['headers']['status-code']);
+
+ // List should still work and reflect at least one less document compared to earlier pages (best-effort)
+ $list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [Query::limit(5)->toString()]
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ }
+
+ #[Depends('testCreateCollectionSample')]
+ public function testDocumentPermissions(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create doc readable only by a specific user
+ $docId = ID::unique();
+ $vector = array_fill(0, 1536, 0.0);
+ $vector[0] = 1.0;
+ $create = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => $docId,
+ 'data' => [
+ 'embeddings' => $vector,
+ 'metadata' => ['scope' => 'private']
+ ],
+ 'permissions' => [
+ Permission::read(Role::user($this->getUser()['$id']))
+ ]
+ ]);
+ $this->assertEquals(201, $create['headers']['status-code']);
+
+ $guest = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$docId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ]);
+ $this->assertEquals(404, $guest['headers']['status-code']);
+
+ // GET with key should succeed regardless of document user-level permission
+ $withKey = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$docId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $withKey['headers']['status-code']);
+ }
+
+ #[Depends('testCreateDatabase')]
+ public function testCreateCollection(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for SUCCESS
+ */
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'documentSecurity' => true,
+ 'dimension' => 1536,
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ ]);
+
+ $this->assertEquals(201, $movies['headers']['status-code']);
+ $this->assertEquals($movies['body']['name'], 'Movies');
+
+ $actors = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Actors',
+ 'documentSecurity' => true,
+ 'dimension' => 1536,
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ ]);
+
+ $this->assertEquals(201, $actors['headers']['status-code']);
+ $this->assertEquals($actors['body']['name'], 'Actors');
+
+ return [
+ 'databaseId' => $databaseId,
+ 'moviesId' => $movies['body']['$id'],
+ 'actorsId' => $actors['body']['$id'],
+ ];
+ }
+
+ public function testCreateDatabaseSample(): array
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Sample VectorsDB'
+ ]);
+
+ $this->assertNotEmpty($database['body']['$id']);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Sample VectorsDB', $database['body']['name']);
+ $this->assertEquals('vectorsdb', $database['body']['type']);
+
+ return ['databaseId' => $database['body']['$id']];
+ }
+
+ #[Depends('testCreateDatabaseSample')]
+ public function testCreateCollectionSample(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Sample Collection',
+ 'dimension' => 1536,
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $this->assertEquals('Sample Collection', $collection['body']['name']);
+ $this->assertEquals(1536, $collection['body']['dimension']);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collection['body']['$id'],
+ ];
+ }
+
+ public function testCreateMultipleDatabasesWithCollections(): array
+ {
+ $projectId = $this->getProject()['$id'];
+ $apiKey = $this->getProject()['apiKey'];
+ $userId = $this->getUser()['$id'];
+
+ /**
+ * Helper to create a database
+ */
+ $createDatabase = function (string $name) use ($projectId, $apiKey) {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => $name
+ ]);
+
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $this->assertEquals('vectorsdb', $db['body']['type']);
+ $this->assertEquals($name, $db['body']['name']);
+ $this->assertNotEmpty($db['body']['$id']);
+
+ return $db['body']['$id'];
+ };
+
+ /**
+ * Helper to create a collection
+ */
+ $createCollection = function (string $databaseId, string $name, int $dimensions = 1536) use ($projectId, $apiKey, $userId) {
+ $res = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => $name,
+ 'documentSecurity' => true,
+ 'dimension' => $dimensions,
+ 'permissions' => [
+ Permission::create(Role::user($userId)),
+ ],
+ ]);
+
+ $this->assertEquals(201, $res['headers']['status-code']);
+ $this->assertEquals($name, $res['body']['name']);
+
+ return $res['body']['$id'];
+ };
+
+ /**
+ * === Database 1: MediaDB ===
+ */
+ $mediaDbId = $createDatabase('MediaDB');
+
+ $mediaCollections = ['Movies', 'Actors', 'Directors'];
+ $mediaCollectionIds = [];
+
+ foreach ($mediaCollections as $col) {
+ $mediaCollectionIds[$col] = $createCollection($mediaDbId, $col);
+ }
+
+ /**
+ * === Database 2: ContentDB ===
+ */
+ $contentDbId = $createDatabase('ContentDB');
+
+ $contentCollections = ['Articles', 'Authors'];
+ $contentCollectionIds = [];
+
+ foreach ($contentCollections as $col) {
+ $contentCollectionIds[$col] = $createCollection($contentDbId, $col);
+ }
+
+ // Create a tiny-dimension collection and insert a document to validate vector and object attributes
+ $tinyCollectionName = 'VectorsTiny';
+ $tinyDimensions = 8;
+ $tinyCollectionId = $createCollection($mediaDbId, $tinyCollectionName, $tinyDimensions);
+
+ return [
+ 'databases' => [
+ 'MediaDB' => [
+ 'id' => $mediaDbId,
+ 'collections' => $mediaCollectionIds + ['VectorsTiny' => $tinyCollectionId],
+ ],
+ 'ContentDB' => [
+ 'id' => $contentDbId,
+ 'collections' => $contentCollectionIds,
+ ],
+ ]
+ ];
+ }
+
+ public function testInvalidCollectionDimensions(): void
+ {
+ // dimensions = 0 -> expect 4xx
+ $bad0 = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'BadDims0'
+ ]);
+ $this->assertEquals(201, $bad0['headers']['status-code']);
+ $dbId = $bad0['body']['$id'];
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $dbId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'ZeroDims',
+ 'documentSecurity' => true,
+ 'dimension' => 0,
+ 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))],
+ ]);
+ $this->assertGreaterThanOrEqual(400, $col['headers']['status-code']);
+ $this->assertLessThan(500, $col['headers']['status-code']);
+
+ // dimensions too large -> expect 4xx
+ $col2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $dbId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'HugeDims',
+ 'documentSecurity' => true,
+ 'dimension' => 16001,
+ 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))],
+ ]);
+ $this->assertGreaterThanOrEqual(400, $col2['headers']['status-code']);
+ $this->assertLessThan(500, $col2['headers']['status-code']);
+ }
+
+ public function testSingleDimensionVectorCollection(): void
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'SingleDim'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'OneDim',
+ 'documentSecurity' => true,
+ 'dimension' => 1,
+ 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))],
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Create two docs with 1D embeddings
+ $id1 = ID::unique();
+ $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id1}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => ['embeddings' => [1.0]]
+ ]);
+ $id2 = ID::unique();
+ $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id2}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => ['embeddings' => [0.5]]
+ ]);
+
+ // Query with vectorCosine
+ $res = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [Query::vectorCosine('embeddings', [1.0])->toString(), Query::limit(2)->toString()]
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $res['body']['total']);
+ }
+
+ public function testVectorInvalidValues(): void
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'InvalidVals'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Docs',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))],
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $badPayloads = [
+ ['embeddings' => [INF, 0.0, 0.0]],
+ ['embeddings' => [-INF, 0.0, 0.0]],
+ ['embeddings' => [NAN, 0.0, 0.0]],
+ ['embeddings' => ['x' => 1.0, 'y' => 0.0, 'z' => 0.0]],
+ ['embeddings' => [1.0, null, 0.0]],
+ ['embeddings' => [[1.0], [0.0], [0.0]]],
+ ['embeddings' => [true, false, true]],
+ ['embeddings' => [1.0, '2.0', 3.0]],
+ (function () {
+ $v = [];
+ $v[0] = 1.0;
+ $v[2] = 1.0;
+ return ['embeddings' => $v];
+ })(),
+ ];
+
+ foreach ($badPayloads as $payload) {
+ $resp = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/" . ID::unique(), [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => $payload
+ ]);
+ $this->assertGreaterThanOrEqual(400, $resp['headers']['status-code']);
+ $this->assertLessThan(500, $resp['headers']['status-code']);
+ }
+ }
+
+ public function testVectorAllZerosAndQuery(): void
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'ZerosDB'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Zeros',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))],
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/" . ID::unique(), [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'data' => ['embeddings' => [0.0, 0.0, 0.0]] ]);
+
+ $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/" . ID::unique(), [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'data' => ['embeddings' => [1.0, 0.0, 0.0]] ]);
+
+ $results = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'queries' => [Query::vectorCosine('embeddings', [1.0, 0.0, 0.0])->toString()] ]);
+ $this->assertEquals(200, $results['headers']['status-code']);
+ $this->assertGreaterThan(0, $results['body']['total']);
+ }
+
+ public function testVectorMultipleQueriesRejection(): void
+ {
+ // Create a simple DB and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'MultiQueryDB' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'collectionId' => ID::unique(), 'name' => 'Docs', 'documentSecurity' => true, 'dimension' => 3, 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))] ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Two vector queries simultaneously should fail
+ $fail = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ Query::vectorCosine('embeddings', [1.0, 0.0, 0.0])->toString(),
+ Query::vectorEuclidean('embeddings', [1.0, 0.0, 0.0])->toString()
+ ]
+ ]);
+ $this->assertGreaterThanOrEqual(400, $fail['headers']['status-code']);
+ $this->assertLessThan(500, $fail['headers']['status-code']);
+ }
+
+ public function testVectorQueryOnNonVectorAttribute(): void
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'NonVec' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'collectionId' => ID::unique(), 'name' => 'Docs', 'documentSecurity' => true, 'dimension' => 3, 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))] ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Query on non-vector attribute 'metadata' should fail
+ $fail = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'queries' => [Query::vectorCosine('metadata', [1.0, 0.0, 0.0])->toString()] ]);
+ $this->assertGreaterThanOrEqual(400, $fail['headers']['status-code']);
+ $this->assertLessThan(500, $fail['headers']['status-code']);
+ }
+
+ public function testVectorEmptyQueryCollection(): void
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'EmptyQ' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+ $col = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'collectionId' => ID::unique(), 'name' => 'Docs', 'documentSecurity' => true, 'dimension' => 3, 'permissions' => [Permission::create(Role::user($this->getUser()['$id']))] ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $res = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'queries' => [Query::vectorCosine('embeddings', [1.0, 0.0, 0.0])->toString()] ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals(0, $res['body']['total']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testCreateIndexes(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['moviesId'];
+
+ // HNSW Euclidean
+ $idxEuclidean = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_euclidean',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $idxEuclidean['headers']['status-code']);
+
+ // HNSW Dot (Inner Product)
+ $idxDot = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_dot',
+ 'type' => Database::INDEX_HNSW_DOT,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $idxDot['headers']['status-code']);
+
+ // HNSW Cosine
+ $idxCosine = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_cosine',
+ 'type' => Database::INDEX_HNSW_COSINE,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $idxCosine['headers']['status-code']);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'indexes' => ['embedding_euclidean', 'embedding_dot', 'embedding_cosine']
+ ];
+ }
+
+ #[Depends('testCreateIndexes')]
+ public function testListIndexes(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $keys = array_map(fn ($i) => $i['key'], $list['body']['indexes'] ?? []);
+ foreach ($data['indexes'] as $expectedKey) {
+ $this->assertContains($expectedKey, $keys);
+ }
+ }
+
+ #[Depends('testCreateIndexes')]
+ public function testGetIndexByKey(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $keysToTypes = [
+ 'embedding_euclidean' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'embedding_dot' => Database::INDEX_HNSW_DOT,
+ 'embedding_cosine' => Database::INDEX_HNSW_COSINE,
+ ];
+
+ foreach ($keysToTypes as $key => $type) {
+ $res = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/{$key}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals($key, $res['body']['key']);
+ $this->assertEquals($type, $res['body']['type']);
+ }
+ }
+
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php b/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php
new file mode 100644
index 0000000000..abe4d4968b
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php
@@ -0,0 +1,312 @@
+client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Vector Console DB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Vector Console DB', $database['body']['name']);
+ $this->assertTrue($database['body']['enabled']);
+
+ $databaseId = $database['body']['$id'];
+
+ /**
+ * Test for SUCCESS
+ */
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ $this->assertEquals(201, $movies['headers']['status-code']);
+ $this->assertEquals($movies['body']['name'], 'Movies');
+
+ /**
+ * Test when database is disabled but can still create collections
+ */
+ $database = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Vector Console DB Updated',
+ 'enabled' => false,
+ ]);
+
+ $this->assertFalse($database['body']['enabled']);
+
+ $tvShows = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'TvShows',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ /**
+ * Test when collection is disabled but can still modify collections
+ */
+ $database = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $movies['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Movies',
+ 'enabled' => false,
+ ]);
+
+ $this->assertEquals(201, $tvShows['headers']['status-code']);
+ $this->assertEquals($tvShows['body']['name'], 'TvShows');
+
+ return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testListCollection(array $data)
+ {
+ /**
+ * Test when database is disabled but can still call list collections
+ */
+ $databaseId = $data['databaseId'];
+
+ $collections = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $collections['headers']['status-code']);
+ $this->assertEquals(2, $collections['body']['total']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $moviesCollectionId = $data['moviesId'];
+
+ /**
+ * Test when database and collection are disabled but can still call get collection
+ */
+ $collection = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $collection['headers']['status-code']);
+ $this->assertEquals('Movies', $collection['body']['name']);
+ $this->assertEquals($moviesCollectionId, $collection['body']['$id']);
+ $this->assertFalse($collection['body']['enabled']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testUpdateCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $moviesCollectionId = $data['moviesId'];
+
+ /**
+ * Test When database and collection are disabled but can still call update collection
+ */
+ $collection = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Movies Updated',
+ 'enabled' => false
+ ]);
+
+ $this->assertEquals(200, $collection['headers']['status-code']);
+ $this->assertEquals('Movies Updated', $collection['body']['name']);
+ $this->assertEquals($moviesCollectionId, $collection['body']['$id']);
+ $this->assertFalse($collection['body']['enabled']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testDeleteCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $tvShowsId = $data['tvShowsId'];
+
+ /**
+ * Test when database and collection are disabled but can still call delete collection
+ */
+ $response = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $tvShowsId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+ $this->assertEquals($response['body'], "");
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetDatabaseUsage(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for FAILURE
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '32h'
+ ]);
+
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ /**
+ * Test for SUCCESS
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(11, count($response['body']));
+ $this->assertEquals('24h', $response['body']['range']);
+ $this->assertIsNumeric($response['body']['documentsTotal']);
+ $this->assertIsNumeric($response['body']['collectionsTotal']);
+ $this->assertIsArray($response['body']['collections']);
+ $this->assertIsArray($response['body']['documents']);
+ }
+
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollectionUsage(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for FAILURE
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '32h'
+ ]);
+
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/randomCollectionId/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+
+ $this->assertEquals(404, $response['headers']['status-code']);
+
+ /**
+ * Test for SUCCESS
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(3, count($response['body']));
+ $this->assertEquals('24h', $response['body']['range']);
+ $this->assertIsNumeric($response['body']['documentsTotal']);
+ $this->assertIsArray($response['body']['documents']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollectionLogs(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for SUCCESS
+ */
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::offset(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomClientTest.php
new file mode 100644
index 0000000000..b27cb420b4
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomClientTest.php
@@ -0,0 +1,205 @@
+client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database'
+ ]);
+
+ $databaseId = $database['body']['$id'];
+
+ // Collection aliases write to create, update, delete
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::write(Role::user($this->getUser()['$id'])),
+ ],
+ ]);
+
+ $moviesId = $movies['body']['$id'];
+
+ $this->assertContains(Permission::create(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+ $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+ $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+
+ // VectorsDB uses fixed schema (embeddings, metadata). No attribute creation needed.
+
+ // Document aliases write to update, delete
+ $document1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['k' => 'v'],
+ ],
+ 'permissions' => [
+ Permission::write(Role::user($this->getUser()['$id'])),
+ ]
+ ]);
+
+ $this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+ $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+ $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+
+ /**
+ * Test for FAILURE
+ */
+
+ // Document does not allow create permission
+ $document2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['k' => 'v'],
+ ],
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ]
+ ]);
+
+ $this->assertEquals(400, $document2['headers']['status-code']);
+ }
+
+ public function testUpdateWithoutPermission(): array
+ {
+ // As a part of preparation, we get ID of currently logged-in user
+ $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $userId = $response['body']['$id'];
+
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::custom('permissionCheckDatabase'),
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Test Database', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ // Create collection
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::custom('permissionCheck'),
+ 'name' => 'permissionCheck',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Creating document by server, give read permission to our user + some other user
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documentId' => ID::custom('permissionCheckDocument'),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['name' => 'AppwriteBeginner'],
+ ],
+ 'permissions' => [
+ Permission::read(Role::user(ID::custom('user2'))),
+ Permission::read(Role::user($userId)),
+ Permission::update(Role::user($userId)),
+ Permission::delete(Role::user($userId)),
+ ],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Update document
+ // This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
+ $response = $this->client->call(Client::METHOD_PATCH, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['name' => 'AppwriteExpert'],
+ ]
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Get name of the document, should be the new one
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals("AppwriteExpert", $response['body']['metadata']['name']);
+
+ // Cleanup to prevent collision with other tests
+ // Delete collection
+ $response = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/permissionCheck', 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']);
+
+
+ // Wait for database worker to finish deleting collection
+ sleep(2);
+
+ // Make sure collection has been deleted
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/permissionCheck', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+ $this->assertEquals(404, $response['headers']['status-code']);
+
+ return [];
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomServerTest.php
new file mode 100644
index 0000000000..9564b76079
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/DatabasesCustomServerTest.php
@@ -0,0 +1,964 @@
+client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::custom('first'),
+ 'name' => 'Test 1',
+ ]);
+ $this->assertEquals(201, $db1['headers']['status-code']);
+ $this->assertEquals('Test 1', $db1['body']['name']);
+ $this->assertEquals('vectorsdb', $db1['body']['type']);
+ // Validate database response model fields on create
+ $this->assertArrayHasKey('$id', $db1['body']);
+ $this->assertArrayHasKey('$createdAt', $db1['body']);
+ $this->assertArrayHasKey('$updatedAt', $db1['body']);
+ $this->assertArrayHasKey('enabled', $db1['body']);
+
+ $db2 = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::custom('second'),
+ 'name' => 'Test 2',
+ ]);
+ $this->assertEquals(201, $db2['headers']['status-code']);
+ $this->assertEquals('Test 2', $db2['body']['name']);
+ $this->assertEquals('vectorsdb', $db2['body']['type']);
+
+ $list = $this->client->call(Client::METHOD_GET, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsInt($list['body']['total']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+ $this->assertIsArray($list['body']['databases']);
+ $this->assertArrayHasKey('$id', $list['body']['databases'][0]);
+ $this->assertArrayHasKey('name', $list['body']['databases'][0]);
+ $this->assertArrayHasKey('type', $list['body']['databases'][0]);
+
+ return ['databaseId' => $db1['body']['$id']];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testGetDatabase(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $res = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals($databaseId, $res['body']['$id']);
+ $this->assertEquals('Test 1', $res['body']['name']);
+ $this->assertEquals('vectorsdb', $res['body']['type']);
+ return ['databaseId' => $databaseId];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testUpdateDatabase(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $res = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Updated',
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals('Test 1 Updated', $res['body']['name']);
+ $this->assertEquals('vectorsdb', $res['body']['type']);
+ return ['databaseId' => $databaseId];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testDeleteDatabase(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ $this->assertEquals("", $del['body']);
+
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $get['headers']['status-code']);
+ }
+
+ public function testCollectionsCRUD(): array
+ {
+ // Create database for collections tests
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Collections DB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ // Create two collections
+ $col1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1',
+ 'collectionId' => ID::custom('first'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ ]);
+ $this->assertEquals(201, $col1['headers']['status-code']);
+ // Validate collection response model on create
+ $this->assertArrayHasKey('$id', $col1['body']);
+ $this->assertArrayHasKey('$createdAt', $col1['body']);
+ $this->assertArrayHasKey('$updatedAt', $col1['body']);
+ $this->assertArrayHasKey('enabled', $col1['body']);
+ $this->assertArrayHasKey('documentSecurity', $col1['body']);
+ $this->assertArrayHasKey('dimension', $col1['body']);
+
+ $col2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 2',
+ 'collectionId' => ID::custom('second'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ ]);
+ $this->assertEquals(201, $col2['headers']['status-code']);
+ $this->assertArrayHasKey('$id', $col2['body']);
+ $this->assertArrayHasKey('$createdAt', $col2['body']);
+ $this->assertArrayHasKey('$updatedAt', $col2['body']);
+
+ // List collections
+ $list = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsInt($list['body']['total']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+ $this->assertIsArray($list['body']['collections']);
+ $this->assertArrayHasKey('$id', $list['body']['collections'][0]);
+ $this->assertArrayHasKey('name', $list['body']['collections'][0]);
+ $this->assertArrayHasKey('dimension', $list['body']['collections'][0]);
+
+ // Get collection
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($col1['body']['$id'], $get['body']['$id']);
+ $this->assertEquals('Test 1', $get['body']['name']);
+ $this->assertEquals(3, $get['body']['dimension']);
+
+ // Update collection (name only)
+ $upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Updated',
+ ]);
+ $this->assertEquals(200, $upd['headers']['status-code']);
+ $this->assertEquals('Test 1 Updated', $upd['body']['name']);
+ $this->assertArrayHasKey('$updatedAt', $upd['body']);
+
+ // Delete collection
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $col2['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ $this->assertEquals("", $del['body']);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $col1['body']['$id'],
+ ];
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testUpdateCollectionMore(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Update collection name and dimensions
+ $upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Renamed',
+ 'dimension' => 4,
+ ]);
+ $this->assertEquals(200, $upd['headers']['status-code']);
+ $this->assertEquals('Test 1 Renamed', $upd['body']['name']);
+ $this->assertEquals(4, $upd['body']['dimension']);
+
+ // Read back to confirm
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('Test 1 Renamed', $get['body']['name']);
+ $this->assertEquals(4, $get['body']['dimension']);
+
+ return $data;
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testUpdateCollectionEnabledFlag(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Disable collection
+ $disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Updated',
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $disable['headers']['status-code']);
+ $this->assertFalse($disable['body']['enabled']);
+
+ // Re-enable collection
+ $enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Updated',
+ 'enabled' => true,
+ ]);
+ $this->assertEquals(200, $enable['headers']['status-code']);
+ $this->assertTrue($enable['body']['enabled']);
+
+ return $data;
+ }
+
+ public function testUpdateDatabaseNameAndEnabled(): void
+ {
+ // Create isolated database for this test to avoid ordering conflicts
+ $create = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Update DB',
+ ]);
+ $this->assertEquals(201, $create['headers']['status-code']);
+ $databaseId = $create['body']['$id'];
+
+ // Update name
+ $rename = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ ]);
+ $this->assertEquals(200, $rename['headers']['status-code']);
+ $this->assertEquals('Test DB Renamed', $rename['body']['name']);
+
+ // Toggle enabled off then on
+ $disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $disable['headers']['status-code']);
+ $this->assertFalse($disable['body']['enabled']);
+
+ $enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ 'enabled' => true,
+ ]);
+ $this->assertEquals(200, $enable['headers']['status-code']);
+ $this->assertTrue($enable['body']['enabled']);
+
+ // Cleanup
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testRecreateIndex(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create a new index variant
+ $create = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_euclidean_v2',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+
+ // Ensure it exists
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('embedding_euclidean_v2', $get['body']['key']);
+
+ // Delete it
+ $del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testIndexesCRUD(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create indexes
+ $eu = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_euclidean',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $eu['headers']['status-code']);
+
+ $dot = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_dot',
+ 'type' => Database::INDEX_HNSW_DOT,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $dot['headers']['status-code']);
+
+ $cos = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_cosine',
+ 'type' => Database::INDEX_HNSW_COSINE,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $cos['headers']['status-code']);
+
+ // List indexes
+ $list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsArray($list['body']['indexes']);
+ $keys = array_map(fn ($i) => $i['key'], $list['body']['indexes']);
+ $this->assertContains('embedding_euclidean', $keys);
+ $this->assertContains('embedding_dot', $keys);
+ $this->assertContains('embedding_cosine', $keys);
+
+ // Get index by key
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('embedding_euclidean', $get['body']['key']);
+ $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $get['body']['type']);
+
+ // Delete index
+ $del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ sleep(4);
+ // Ensure it's gone
+ $getMissing = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $getMissing['headers']['status-code']);
+ }
+
+ public function testBulkCreate(): array
+ {
+ // Setup: create isolated database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'BulkDBCreate'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColCreate',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $docs = [
+ [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['group' => 'bulkA'],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['group' => 'bulkB'],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ ];
+
+ $res = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs
+ ]);
+
+ $this->assertEquals(201, $res['headers']['status-code']);
+ $this->assertIsInt($res['body']['total'] ?? 0);
+ $this->assertGreaterThanOrEqual(2, $res['body']['total']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+
+ $ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
+ $this->assertNotEmpty($ids[0]);
+ $this->assertNotEmpty($ids[1]);
+
+ // Fetch and validate persisted data via GET
+ foreach ($ids as $i => $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($id, $get['body']['$id']);
+ $this->assertIsArray($get['body']['embeddings']);
+ $this->assertCount(3, $get['body']['embeddings']);
+ $this->assertArrayHasKey('group', $get['body']['metadata']);
+ }
+
+ return [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'bulkIds' => $ids ];
+ }
+
+ public function testCreateTextEmbeddingsSuccessAndErrors(): void
+ {
+ // Setup new database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'EmbedDB',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'EmbedCol',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Success: two embeddings
+ $this->assertEventually(function () {
+ $ok = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'embeddinggemma',
+ 'texts' => [
+ 'hello world',
+ 'second sentence',
+ ],
+ ]);
+ $this->assertEquals(200, $ok['headers']['status-code']);
+ $this->assertIsInt($ok['body']['total'] ?? 0);
+ $this->assertEquals(2, $ok['body']['total']);
+ $this->assertIsArray($ok['body']['embeddings']);
+ $this->assertCount(2, $ok['body']['embeddings']);
+ foreach ($ok['body']['embeddings'] as $embed) {
+ $this->assertIsString($embed['model']);
+ $this->assertIsInt($embed['dimension']);
+ $this->assertIsArray($embed['embedding']);
+ $this->assertGreaterThan(0, count($embed['embedding']));
+ $this->assertArrayHasKey('error', $embed);
+ }
+ }, 3000, 100);
+
+ // Error: missing texts payload
+ $missingTexts = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], []);
+ $this->assertEquals(400, $missingTexts['headers']['status-code']);
+
+ // Error: invalid texts item type (must be strings)
+ $invalidItem = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'embeddinggemma',
+ 'texts' => [
+ 'valid text',
+ 123, // invalid, not a string
+ ],
+ ]);
+ $this->assertEquals(400, $invalidItem['headers']['status-code']);
+
+ // Error: unknown embedding model
+ $unknownModel = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'nonexistent-model',
+ 'texts' => ['hello'],
+ ]);
+ $this->assertEquals(400, $unknownModel['headers']['status-code']);
+ }
+
+ public function testBulkUpsert(): void
+ {
+ // Setup fresh db/collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpsert' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColUpsert',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $docs = [
+ [
+ 'embeddings' => [0.5, 0.5, 0.0],
+ 'metadata' => ['group' => 'bulkA', 'updated' => true],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ [
+ 'embeddings' => [0.2, 0.8, 0.0],
+ 'metadata' => ['group' => 'bulkB', 'updated' => true],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ ];
+
+ $res = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs
+ ]);
+
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+ $this->assertTrue($res['body']['documents'][0]['metadata']['updated']);
+ $this->assertTrue($res['body']['documents'][1]['metadata']['updated']);
+
+ // Fetch and validate updated content
+ $ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertTrue($get['body']['metadata']['updated']);
+ }
+
+ // Perform another bulk upsert to mutate the same documents
+ $docs2 = [
+ [ 'embeddings' => [0.6, 0.4, 0.0], 'metadata' => ['updatedAgain' => true] ],
+ [ 'embeddings' => [0.3, 0.7, 0.0], 'metadata' => ['updatedAgain' => true] ],
+ ];
+ $res2 = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs2
+ ]);
+ $this->assertEquals(200, $res2['headers']['status-code']);
+ $this->assertIsArray($res2['body']['documents']);
+ $this->assertCount(2, $res2['body']['documents']);
+
+ // Fetch again and assert second update persisted
+ $ids2 = array_map(fn ($d) => $d['$id'], $res2['body']['documents']);
+ foreach ($ids2 as $id) {
+ $get2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get2['headers']['status-code']);
+ $this->assertTrue($get2['body']['metadata']['updatedAgain']);
+ }
+ }
+
+ public function testBulkUpdate(): void
+ {
+ // Setup: create db/collection and two docs
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpdate' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColUpdate',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => [
+ ['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
+ ['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
+ ]
+ ]);
+ $this->assertEquals(200, $seed['headers']['status-code']);
+ $ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
+
+ $res = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [ 'metadata' => ['bulkUpdated' => true] ],
+ 'queries' => [
+ \Utopia\Database\Query::equal('$id', $ids)->toString()
+ ]
+ ]);
+
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+ foreach ($res['body']['documents'] as $doc) {
+ $this->assertTrue($doc['metadata']['bulkUpdated']);
+ }
+
+ // Fetch by IDs and assert update persisted
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertTrue($get['body']['metadata']['bulkUpdated']);
+ }
+ }
+
+ public function testBulkDelete(): void
+ {
+ // Setup: create db/collection and two docs
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBDelete' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColDelete',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => [
+ ['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
+ ['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
+ ]
+ ]);
+ $this->assertEquals(200, $seed['headers']['status-code']);
+ $ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
+
+ $res = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ \Utopia\Database\Query::equal('$id', $ids)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsInt($res['body']['total'] ?? 0);
+ $this->assertGreaterThanOrEqual(2, $res['body']['total']);
+
+ // Ensure they are deleted
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $get['headers']['status-code']);
+ }
+ }
+
+ public function testCustomTimestamps(): void
+ {
+ // Setup: create database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'TimestampTestDB'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'TimestampTestCollection',
+ 'documentSecurity' => true,
+ 'dimension' => 1536,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Test: Create document with custom timestamps using PUT (upsert)
+ $customCreatedAt = '1970-01-01T00:00:00.000+00:00';
+ $customUpdatedAt = '1970-01-01T00:00:00.000+00:00';
+ $vector = array_fill(0, 1536, 0.0);
+ $vector[0] = 1.0;
+ $documentId = ID::unique();
+
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => $documentId,
+ 'data' => [
+ '$createdAt' => $customCreatedAt,
+ '$updatedAt' => $customUpdatedAt,
+ 'embeddings' => $vector,
+ 'metadata' => ['test' => 'custom_timestamps']
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+ $documentId = $doc['body']['$id'];
+ $this->assertNotEmpty($documentId);
+
+ // Verify timestamps were set correctly
+ $this->assertEquals($customCreatedAt, $doc['body']['$createdAt'], 'CreatedAt should match custom timestamp');
+ $this->assertEquals($customUpdatedAt, $doc['body']['$updatedAt'], 'UpdatedAt should match custom timestamp');
+
+ // Fetch document and verify timestamps persist
+ $fetched = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $fetched['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $fetched['body']['$createdAt'], 'CreatedAt should persist after fetch');
+ $this->assertEquals($customUpdatedAt, $fetched['body']['$updatedAt'], 'UpdatedAt should persist after fetch');
+
+ // Test: Update document with new custom timestamps
+ $newCustomUpdatedAt = '2000-01-01T12:00:00.000+00:00';
+ $vector2 = array_fill(0, 1536, 0.0);
+ $vector2[1] = 1.0;
+
+ $updated = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [
+ '$createdAt' => $customCreatedAt, // Keep original createdAt
+ '$updatedAt' => $newCustomUpdatedAt, // Update updatedAt
+ 'embeddings' => $vector2,
+ 'metadata' => ['test' => 'updated_timestamps']
+ ]
+ ]);
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $updated['body']['$createdAt'], 'CreatedAt should remain unchanged');
+ $this->assertEquals($newCustomUpdatedAt, $updated['body']['$updatedAt'], 'UpdatedAt should be updated to new custom timestamp');
+
+ // Final verification
+ $final = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $final['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $final['body']['$createdAt'], 'CreatedAt should persist through updates');
+ $this->assertEquals($newCustomUpdatedAt, $final['body']['$updatedAt'], 'UpdatedAt should reflect the latest custom timestamp');
+ }
+
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsGuestTest.php
new file mode 100644
index 0000000000..9335b7f55b
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsGuestTest.php
@@ -0,0 +1,280 @@
+authorization)) {
+ return $this->authorization;
+ }
+
+ $this->authorization = new Authorization();
+
+ return $this->authorization;
+ }
+
+ public function createCollection(): array
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'VectorGuestDB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('VectorGuestDB', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $publicMovies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+ $privateMovies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+
+ $publicCollection = ['id' => $publicMovies['body']['$id']];
+ $privateCollection = ['id' => $privateMovies['body']['$id']];
+
+ return [
+ 'databaseId' => $databaseId,
+ 'publicCollectionId' => $publicCollection['id'],
+ 'privateCollectionId' => $privateCollection['id'],
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())]],
+ [[Permission::read(Role::users())]],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
+ ];
+ }
+
+ #[DataProvider('permissionsProvider')]
+ public function testReadDocuments($permissions)
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $publicResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions,
+ ]);
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions,
+ ]);
+
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicDocuments = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+ $privateDocuments = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(1, $publicDocuments['body']['total']);
+ $this->assertEquals($permissions, $publicDocuments['body']['documents'][0]['$permissions']);
+
+ if (\in_array(Permission::read(Role::any()), $permissions)) {
+ $this->assertEquals(1, $privateDocuments['body']['total']);
+ $this->assertEquals($permissions, $privateDocuments['body']['documents'][0]['$permissions']);
+ } else {
+ $this->assertEquals(0, $privateDocuments['body']['total']);
+ }
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocument()
+ {
+ $data = $this->createCollection();
+ $publicCollectionId = $data['publicCollectionId'];
+ $privateCollectionId = $data['privateCollectionId'];
+ $databaseId = $data['databaseId'];
+
+ $roles = $this->getAuthorization()->getRoles();
+ $this->getAuthorization()->cleanRoles();
+
+ $publicResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ]
+ ]);
+
+ $publicDocumentId = $publicResponse['body']['$id'];
+ $this->assertEquals(201, $publicResponse['headers']['status-code']);
+
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+
+ $this->assertEquals(401, $privateResponse['headers']['status-code']);
+
+ // Create a document in private collection with API key so we can test that update and delete are also not allowed
+ $privateResponse = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 0.0, 1.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $privateResponse['headers']['status-code']);
+ $privateDocumentId = $privateResponse['body']['$id'];
+
+ $publicDocument = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'data' => [
+ 'embeddings' => [0.5, 0.5, 0.0],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ ]);
+
+ $this->assertEquals(200, $publicDocument['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $publicDocument['body']['metadata']['title']);
+
+ $privateDocument = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'data' => [
+ 'embeddings' => [0.2, 0.3, 0.5],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ ]);
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ $publicDocument = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(204, $publicDocument['headers']['status-code']);
+
+ $privateDocument = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(401, $privateDocument['headers']['status-code']);
+
+ foreach ($roles as $role) {
+ $this->getAuthorization()->addRole($role);
+ }
+ }
+
+ public function testWriteDocumentWithPermissions()
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'VectorGuestPermsWrite',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('VectorGuestPermsWrite', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ 'documentSecurity' => true
+ ]);
+
+ $moviesId = $movies['body']['$id'];
+
+ $document = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Thor: Ragnarok'],
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ ]
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $this->assertEquals('Thor: Ragnarok', $document['body']['metadata']['title']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsMemberTest.php b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsMemberTest.php
new file mode 100644
index 0000000000..cbc2add857
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsMemberTest.php
@@ -0,0 +1,196 @@
+ $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ ];
+ }
+
+ public static function permissionsProvider(): array
+ {
+ return [
+ [[Permission::read(Role::any())], 1, 1, 1],
+ [[Permission::read(Role::users())], 2, 2, 2],
+ [[Permission::read(Role::user(ID::custom('random')))], 3, 3, 2],
+ [[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], 4, 4, 2],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 5, 5, 2],
+ [[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], 6, 6, 2],
+ [[Permission::update(Role::any()), Permission::delete(Role::any())], 7, 7, 2],
+ [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], 8, 8, 3],
+ [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], 9, 9, 4],
+ [[Permission::read(Role::user(ID::custom('user1')))], 10, 10, 5],
+ [[Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], 11, 11, 6],
+ [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())], 12, 12, 7],
+ ];
+ }
+
+ /**
+ * Setup database
+ *
+ * Data providers lose object state so explicitly pass [$users, $collections] to each iteration
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function testSetupDatabase(): array
+ {
+ $this->createUsers();
+
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', $this->getServerHeader(), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $databaseId = $db['body']['$id'];
+
+ $public = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $public['headers']['status-code']);
+ $this->collections = ['public' => $public['body']['$id']];
+
+ $private = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Private Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::users()),
+ Permission::create(Role::users()),
+ Permission::update(Role::users()),
+ Permission::delete(Role::users()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $private['headers']['status-code']);
+ $this->collections['private'] = $private['body']['$id'];
+
+ $doconly = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Document Only Movies',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $private['headers']['status-code']);
+ $this->collections['doconly'] = $doconly['body']['$id'];
+
+ return [
+ 'users' => $this->users,
+ 'collections' => $this->collections,
+ 'databaseId' => $databaseId
+ ];
+ }
+
+ /**
+ * Data provider params are passed before test dependencies.
+ */
+ #[DataProvider('permissionsProvider')]
+ #[Depends('testSetupDatabase')]
+ public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
+ {
+ $users = $data['users'];
+ $collections = $data['collections'];
+ $databaseId = $data['databaseId'];
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 0.0, 1.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ 'permissions' => $permissions
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ /**
+ * Check "any" permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($anyCount, $documents['body']['total']);
+
+ /**
+ * Check "users" permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($usersCount, $documents['body']['total']);
+
+ /**
+ * Check "user:user1" document only permission collection
+ */
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals($docOnlyCount, $documents['body']['total']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsScope.php b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsScope.php
new file mode 100644
index 0000000000..be1800b654
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsScope.php
@@ -0,0 +1,87 @@
+client->call(Client::METHOD_POST, '/account', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-dev-key' => $this->getProject()['devKey'] ?? '',
+ ], [
+ 'userId' => $id,
+ 'email' => $email,
+ 'password' => $password
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], [
+ 'email' => $email,
+ 'password' => $password,
+ ]);
+
+ $session = $session['cookies']['a_session_' . $this->getProject()['$id']];
+
+ $user = [
+ '$id' => $user['body']['$id'],
+ 'email' => $user['body']['email'],
+ 'session' => $session,
+ ];
+ $this->users[$id] = $user;
+
+ return $user;
+ }
+
+ public function getCreatedUser(string $id): array
+ {
+ return $this->users[$id] ?? [];
+ }
+
+ public function createTeam(string $id, string $name): array
+ {
+ $team = $this->client->call(Client::METHOD_POST, '/teams', $this->getServerHeader(), [
+ 'teamId' => $id,
+ 'name' => $name
+ ]);
+ $this->teams[$id] = $team['body'];
+
+ return $team['body'];
+ }
+
+ public function addToTeam(string $user, string $team, array $roles = []): array
+ {
+ $membership = $this->client->call(Client::METHOD_POST, '/teams/' . $team . '/memberships', $this->getServerHeader(), [
+ 'teamId' => $team,
+ 'email' => $this->getCreatedUser($user)['email'],
+ 'roles' => $roles,
+ 'url' => 'http://localhost:5000/join-us#title'
+ ]);
+
+ return [
+ 'user' => $membership['body']['userId'],
+ 'membership' => $membership['body']['$id']
+ ];
+ }
+
+ public function getServerHeader(): array
+ {
+ return [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ];
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsTeamTest.php b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsTeamTest.php
new file mode 100644
index 0000000000..4091ea7140
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Permissions/DatabasesPermissionsTeamTest.php
@@ -0,0 +1,199 @@
+ $this->createTeam('team1', 'Team 1'),
+ 'team2' => $this->createTeam('team2', 'Team 2'),
+ ];
+ }
+
+ public function createUsers(): array
+ {
+ return [
+ 'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
+ 'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
+ 'user3' => $this->createUser('user3', 'sit@ipsum.com'),
+ ];
+ }
+
+ public function createCollections($teams)
+ {
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', $this->getServerHeader(), [
+ 'databaseId' => $this->databaseId,
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+
+ $collection1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::custom('collection1'),
+ 'name' => 'Collection 1',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::team($teams['team1']['$id'])),
+ Permission::create(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::update(Role::team($teams['team1']['$id'], 'admin')),
+ Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
+ ],
+ ]);
+
+ $this->collections['collection1'] = $collection1['body']['$id'];
+
+ $collection2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
+ 'collectionId' => ID::custom('collection2'),
+ 'name' => 'Collection 2',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::team($teams['team2']['$id'])),
+ Permission::create(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::update(Role::team($teams['team2']['$id'], 'owner')),
+ Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
+ ]
+ ]);
+
+ $this->collections['collection2'] = $collection2['body']['$id'];
+
+ return $this->collections;
+ }
+
+ /*
+ * $success = can $user read from $collection
+ * [$user, $collection, $success]
+ */
+ public static function readDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', true],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', true],
+ ];
+ }
+
+ /*
+ * $success = can $user write to $collection
+ * [$user, $collection, $success]
+ */
+ public static function writeDocumentsProvider(): array
+ {
+ return [
+ ['user1', 'collection1', true],
+ ['user2', 'collection1', false],
+ ['user3', 'collection1', false],
+ ['user1', 'collection2', false],
+ ['user2', 'collection2', true],
+ ['user3', 'collection2', false],
+ ];
+ }
+
+ /**
+ * Setup database
+ *
+ * Data providers lose object state
+ * so explicitly pass $users to each iteration
+ * @return array $users
+ */
+ public function testSetupDatabase(): array
+ {
+ $this->createUsers();
+ $this->createTeams();
+
+ $this->addToTeam('user1', 'team1', ['admin']);
+ $this->addToTeam('user2', 'team2', ['owner']);
+
+ // user3 in both teams but with no roles
+ $this->addToTeam('user3', 'team1');
+ $this->addToTeam('user3', 'team2');
+
+ $this->createCollections($this->teams);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Lorem'],
+ ],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['title' => 'Ipsum'],
+ ],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ return $this->users;
+ }
+
+ /**
+ * Data provider params are passed before test dependencies.
+ */
+ #[Depends('testSetupDatabase')]
+ #[DataProvider('readDocumentsProvider')]
+ public function testReadDocuments($user, $collection, $success, $users)
+ {
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ]);
+
+ if ($success) {
+ $this->assertCount(1, $documents['body']['documents']);
+ } else {
+ $this->assertEquals(401, $documents['headers']['status-code']);
+ }
+ }
+
+ #[Depends('testSetupDatabase')]
+ #[DataProvider('writeDocumentsProvider')]
+ public function testWriteDocuments($user, $collection, $success, $users)
+ {
+ $documents = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $this->databaseId . '/collections/' . $collection . '/documents', [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.2, 0.3, 0.5],
+ 'metadata' => ['title' => 'Ipsum'],
+ ],
+ ]);
+
+ if ($success) {
+ $this->assertEquals(201, $documents['headers']['status-code']);
+ } else {
+ // 401 if user is a part of team, 404 otherwise
+ $this->assertContains($documents['headers']['status-code'], [401, 404]);
+ }
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDB/Transactions/ACIDTest.php b/tests/e2e/Services/Databases/VectorsDB/Transactions/ACIDTest.php
new file mode 100644
index 0000000000..aa8d87eb8e
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Transactions/ACIDTest.php
@@ -0,0 +1,528 @@
+client->call(Client::METHOD_POST, '/vectorsdb', 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 for the test
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ // Create a document outside the transaction
+ $existingDocumentId = 'existing_doc';
+ $doc1 = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => $existingDocumentId,
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['email' => 'existing@example.com'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $doc1['headers']['status-code']);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/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 create reuses an existing documentId and should cause the commit to fail
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => 'txn_doc_1',
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['email' => 'newuser@example.com'],
+ ],
+ [
+ '$id' => $existingDocumentId,
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['email' => 'duplicate@example.com'],
+ ],
+ [
+ '$id' => 'txn_doc_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.4),
+ 'metadata' => ['email' => 'should-not-exist@example.com'],
+ ],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code'], 'Adding documents via normal route should succeed. Response: ' . json_encode($response['body']));
+
+ // Attempt to commit - should fail due to duplicate document ID
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Verify NO new documents were created (atomicity)
+ $documents = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(1, $documents['body']['total']);
+ $this->assertEquals('existing@example.com', $documents['body']['documents'][0]['metadata']['email']);
+ }
+
+ /**
+ * Test consistency - schema validation and constraints
+ */
+ public function testConsistency(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Stage operations with valid and invalid data (embedding length mismatch)
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['name' => 'Valid User'],
+ ],
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(2, 0.5), // Invalid dimensions
+ 'metadata' => ['name' => 'Invalid User'],
+ ],
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.6),
+ 'metadata' => ['name' => 'Should Not Persist'],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Attempt to commit - should fail due to invalid embeddings
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, 409, 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, "/vectorsdb/{$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 testIsolation(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create initial document with status metadata
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => 'shared_doc',
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['status' => 'pending'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create first transaction
+ $transaction1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/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'];
+
+ // Transaction 1: update status to approved
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['status' => 'approved'],
+ ],
+ ],
+ ],
+ ]);
+
+ // Commit first transaction
+ $response1 = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Document should reflect the first transaction's update
+ $document = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('approved', $document['body']['metadata']['status']);
+
+ // Create second transaction after first commit
+ $transaction2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/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 2: update status to declined
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['status' => 'declined'],
+ ],
+ ],
+ ],
+ ]);
+
+ // Commit second transaction and ensure isolation guarantees
+ $response2 = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Final document should reflect the second transaction's update
+ $document = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals('declined', $document['body']['metadata']['status']);
+ }
+
+ /**
+ * Test durability - committed data persists
+ */
+ public function testDurability(): void
+ {
+ // Create database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ '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'];
+
+ // Create transaction with multiple operations
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/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'];
+
+ // Create two documents via normal route inside transaction
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => 'durable_doc_1',
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['data' => 'Important data 1'],
+ ],
+ [
+ '$id' => 'durable_doc_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.5),
+ 'metadata' => ['data' => 'Important data 2'],
+ ],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ // Update first document inside the same transaction
+ $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'data' => [
+ 'metadata' => ['data' => 'Updated important data 1'],
+ ],
+ 'transactionId' => $transactionId,
+ ]);
+
+ // Commit transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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']);
+
+ // Verify documents exist and have correct data
+ $document1 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ $document2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ // Further update outside transaction to ensure persistence
+ $update = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'data' => [
+ 'metadata' => ['data' => 'Modified outside transaction'],
+ ],
+ ]);
+ $this->assertEquals(200, $update['headers']['status-code']);
+
+ // Verify the update persisted
+ $document1 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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']['metadata']['data']);
+
+ // List all documents to verify total count
+ $documents = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", 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/VectorsDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/VectorsDB/Transactions/TransactionsBase.php
new file mode 100644
index 0000000000..70150a3bc8
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Transactions/TransactionsBase.php
@@ -0,0 +1,2371 @@
+client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/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, '/vectorsdb/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, '/vectorsdb/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, '/vectorsdb/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']);
+ }
+
+ /**
+ * Test adding operations to a transaction
+ */
+ public function testCreateOperations(): void
+ {
+ // Create database first
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Create a collection for testing
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ '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 valid operations
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['name' => 'Test Document 1']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => 'doc2',
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['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, "/vectorsdb/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' => [
+ 'metadata' => ['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, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['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, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['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, '/vectorsdb', 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, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ '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'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Add operations
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['name' => 'Test Document 1']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => 'doc2',
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['name' => 'Test Document 2']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'update',
+ 'documentId' => 'doc1',
+ 'data' => [
+ 'metadata' => ['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, "/vectorsdb/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, "/vectorsdb/{$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['metadata']['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, "/vectorsdb/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, '/vectorsdb', 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, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Create a collection for rollback test
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Add operations
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['value' => 'Should not exist']
+ ]
+ ]
+ ]
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Rollback the transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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('failed', $response['body']['status']);
+
+ // Verify no documents were created
+ $documents = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction with minimum TTL (60 seconds)
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'ttl' => 60
+ ]);
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Add operation
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['data' => 'Should expire']
+ ]
+ ]
+ ]
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Verify transaction was created with correct expiration
+ $txnDetails = $this->client->call(Client::METHOD_GET, "/vectorsdb/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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [Permission::create(Role::any())],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.1 + ($i * 0.001)),
+ 'metadata' => ['value' => 'Test ' . $i]
+ ]
+ ];
+ }
+
+ // First batch should succeed
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.1 + ($i * 0.001)),
+ 'metadata' => ['value' => 'Test ' . $i]
+ ]
+ ];
+ }
+
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create initial document
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['counter' => 100]
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create two transactions
+ $txn1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $txn2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId1 = $txn1['body']['$id'];
+ $transactionId2 = $txn2['body']['$id'];
+
+ // Both transactions try to update the same document
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['counter' => 200]
+ ]
+ ]
+ ]
+ ]);
+
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['counter' => 300]
+ ]
+ ]
+ ]
+ ]);
+
+ // Commit first transaction
+ $response1 = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/{$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']['metadata']['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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create document
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['data' => 'Original']
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Add update operation to transaction
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['data' => 'Updated in transaction']
+ ]
+ ]
+ ]
+ ]);
+
+ // Delete the document outside of transaction
+ $response = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$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, "/vectorsdb/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']); // Conflict
+ }
+
+ /**
+ * Test bulk operations in transactions
+ */
+ public function testBulkOperations(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create some initial documents
+ for ($i = 1; $i <= 5; $i++) {
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.1 + ($i * 0.01)),
+ 'metadata' => [
+ 'name' => 'Existing ' . $i,
+ 'category' => 'old'
+ ]
+ ]
+ ]);
+ }
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Add bulk operations
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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',
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['name' => 'Bulk 1', 'category' => 'new']
+ ],
+ [
+ '$id' => 'bulk_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['name' => 'Bulk 2', 'category' => 'new']
+ ],
+ [
+ '$id' => 'bulk_3',
+ 'embeddings' => $this->generateEmbeddings(3, 0.4),
+ 'metadata' => ['name' => 'Bulk 3', 'category' => 'new']
+ ],
+ ]
+ ],
+ // Bulk update
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'bulkUpdate',
+ 'data' => [
+ 'queries' => [Query::equal('metadata', [['category' => 'old']])->toString()],
+ 'data' => ['metadata' => ['category' => 'updated']]
+ ]
+ ],
+ // Bulk delete
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'bulkDelete',
+ 'data' => [
+ 'queries' => [Query::equal('$id', ['existing_5'])->toString()]
+ ]
+ ]
+ ]
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Commit transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/{$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) {
+ $category = $doc['metadata']['category'] ?? null;
+ switch ($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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create HNSW index on embeddings
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'key' => 'embeddings_index',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings'],
+ ]);
+
+ sleep(2);
+
+ // Create an existing document
+ $duplicateId = ID::unique();
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documentId' => $duplicateId,
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['email' => 'existing@example.com']
+ ]
+ ]);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Add operations - mix of valid and invalid (duplicate id)
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => ['email' => 'valid1@example.com']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => ['email' => 'valid2@example.com']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => $duplicateId,
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.4),
+ 'metadata' => ['email' => 'existing@example.com']
+ ]
+ ],
+ [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ 'action' => 'create',
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.5),
+ 'metadata' => ['email' => 'valid3@example.com']
+ ]
+ ],
+ ]
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Try to commit - should fail and rollback all operations
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/{$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]['metadata']['email']);
+ }
+
+ /**
+ * Test double commit/rollback attempts
+ */
+ public function testDoubleCommitRollback(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [Permission::create(Role::any())],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Test double commit
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Add operation
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['data' => 'Test']
+ ]
+ ]
+ ]
+ ]);
+
+ // First commit
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/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, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId2 = $transaction2['body']['$id'];
+
+ // First rollback
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Try to update non-existent document - should fail at staging time with early validation
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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' => [
+ 'metadata' => ['data' => 'Should fail']
+ ]
+ ]
+ ]
+ ]);
+
+ $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time
+
+ // Test delete non-existent document - should also fail at staging time with early validation
+ $transaction2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId2 = $transaction2['body']['$id'];
+
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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(404, $response['headers']['status-code']); // Document not found at staging time
+ }
+
+ /**
+ * Test createDocument with transactionId via normal route
+ */
+ public function testCreateDocument(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'documentSecurity' => false,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Create document via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => [
+ '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, "/vectorsdb/{$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, "/vectorsdb/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, "/vectorsdb/{$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']['metadata']['name']);
+ }
+
+ /**
+ * Test updateDocument with transactionId via normal route
+ */
+ public function testUpdateDocument(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create document outside transaction
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => [
+ 'name' => 'Original name',
+ 'counter' => 50,
+ 'category' => 'original'
+ ]
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Update document via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$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' => [
+ 'metadata' => [
+ '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, "/vectorsdb/{$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']['metadata']['name']);
+ $this->assertEquals(50, $response['body']['metadata']['counter']);
+
+ // Commit transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/{$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']['metadata']['name']);
+ $this->assertEquals(150, $response['body']['metadata']['counter']);
+ }
+
+ /**
+ * Test upsertDocument with transactionId via normal route
+ */
+ public function testUpsertDocument(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Upsert document (create) via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => [
+ '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, "/vectorsdb/{$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, "/vectorsdb/{$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' => [
+ 'metadata' => [
+ '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, "/vectorsdb/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, "/vectorsdb/{$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']['metadata']['name']);
+ $this->assertEquals(75, $response['body']['metadata']['counter']);
+ }
+
+ /**
+ * Test deleteDocument with transactionId via normal route
+ */
+ public function testDeleteDocument(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create document outside transaction
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => ['name' => 'Will be deleted']
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Delete document via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$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, "/vectorsdb/{$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, "/vectorsdb/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, "/vectorsdb/{$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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Bulk create via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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',
+ 'embeddings' => $this->generateEmbeddings(3),
+ 'metadata' => [
+ 'name' => 'Bulk created 1',
+ 'category' => 'bulk_created'
+ ]
+ ],
+ [
+ '$id' => 'bulk_create_2',
+ 'embeddings' => $this->generateEmbeddings(3, 0.2),
+ 'metadata' => [
+ 'name' => 'Bulk created 2',
+ 'category' => 'bulk_created'
+ ]
+ ],
+ [
+ '$id' => 'bulk_create_3',
+ 'embeddings' => $this->generateEmbeddings(3, 0.3),
+ 'metadata' => [
+ '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, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::equal('metadata', [['metadata' => ['category' => 'bulk_created']]])->toString()]
+ ]);
+
+ $this->assertEquals(0, $response['body']['total']);
+
+ // Individual document check
+ $response = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$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, "/vectorsdb/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, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::equal('metadata', ['metadata' => ['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, "/vectorsdb/{$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']['metadata']['name']);
+ $this->assertEquals('bulk_created', $response['body']['metadata']['category']);
+ }
+ }
+
+ /**
+ * Test bulkUpdate with transactionId via normal route
+ */
+ public function testBulkUpdate(): void
+ {
+ // Create database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create documents for bulk testing
+ for ($i = 1; $i <= 3; $i++) {
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$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' => [
+ 'embeddings' => $this->generateEmbeddings(3, 0.1 * $i),
+ 'metadata' => [
+ 'name' => 'Bulk doc ' . $i,
+ 'category' => 'bulk_test'
+ ]
+ ]
+ ]);
+ }
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $transactionId = $transaction['body']['$id'];
+
+ // Bulk update via normal route with transactionId
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$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('metadata', ['metadata' => ['category' => 'bulk_test']])->toString()],
+ 'data' => ['metadata' => ['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, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::equal('metadata', ['metadata' => ['category' => 'bulk_test']])->toString()]
+ ]);
+
+ $this->assertEquals(3, $response['body']['total']);
+
+ // Commit transaction
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::equal('metadata', ['metadata' => ['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, '/vectorsdb', 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, "/vectorsdb/{$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',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ ],
+ ]);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create transaction
+ $transaction = $this->client->call(Client::METHOD_POST, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Test 1: Invalid action type
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Test 1: Missing both commit and rollback
+ $response = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, "/vectorsdb/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, '/vectorsdb', 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, '/vectorsdb/transactions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(201, $transaction['headers']['status-code']);
+ $transactionId = $transaction['body']['$id'];
+
+ // Test 1: Non-existent database
+ $response = $this->client->call(Client::METHOD_POST, "/vectorsdb/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, "/vectorsdb/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/VectorsDB/Transactions/TransactionsConsoleClientTest.php b/tests/e2e/Services/Databases/VectorsDB/Transactions/TransactionsConsoleClientTest.php
new file mode 100644
index 0000000000..40ff27c572
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDB/Transactions/TransactionsConsoleClientTest.php
@@ -0,0 +1,14 @@
+client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Vector Console DB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Vector Console DB', $database['body']['name']);
+ $this->assertTrue($database['body']['enabled']);
+
+ $databaseId = $database['body']['$id'];
+
+ /**
+ * Test for SUCCESS
+ */
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Movies',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ $this->assertEquals(201, $movies['headers']['status-code']);
+ $this->assertEquals($movies['body']['name'], 'Movies');
+
+ /**
+ * Test when database is disabled but can still create collections
+ */
+ $database = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Vector Console DB Updated',
+ 'enabled' => false,
+ ]);
+
+ $this->assertFalse($database['body']['enabled']);
+
+ $tvShows = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'TvShows',
+ 'dimension' => 3,
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ /**
+ * Test when collection is disabled but can still modify collections
+ */
+ $database = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $movies['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Movies',
+ 'enabled' => false,
+ ]);
+
+ $this->assertEquals(201, $tvShows['headers']['status-code']);
+ $this->assertEquals($tvShows['body']['name'], 'TvShows');
+
+ return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testListCollection(array $data)
+ {
+ /**
+ * Test when database is disabled but can still call list collections
+ */
+ $databaseId = $data['databaseId'];
+
+ $collections = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $collections['headers']['status-code']);
+ $this->assertEquals(2, $collections['body']['total']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $moviesCollectionId = $data['moviesId'];
+
+ /**
+ * Test when database and collection are disabled but can still call get collection
+ */
+ $collection = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $collection['headers']['status-code']);
+ $this->assertEquals('Movies', $collection['body']['name']);
+ $this->assertEquals($moviesCollectionId, $collection['body']['$id']);
+ $this->assertFalse($collection['body']['enabled']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testUpdateCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $moviesCollectionId = $data['moviesId'];
+
+ /**
+ * Test When database and collection are disabled but can still call update collection
+ */
+ $collection = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'name' => 'Movies Updated',
+ 'enabled' => false
+ ]);
+
+ $this->assertEquals(200, $collection['headers']['status-code']);
+ $this->assertEquals('Movies Updated', $collection['body']['name']);
+ $this->assertEquals($moviesCollectionId, $collection['body']['$id']);
+ $this->assertFalse($collection['body']['enabled']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testDeleteCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $tvShowsId = $data['tvShowsId'];
+
+ /**
+ * Test when database and collection are disabled but can still call delete collection
+ */
+ $response = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $tvShowsId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+ $this->assertEquals($response['body'], "");
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetDatabaseUsage(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for FAILURE
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '32h'
+ ]);
+
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ /**
+ * Test for SUCCESS
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(11, count($response['body']));
+ $this->assertEquals('24h', $response['body']['range']);
+ $this->assertIsNumeric($response['body']['documentsTotal']);
+ $this->assertIsNumeric($response['body']['collectionsTotal']);
+ $this->assertIsArray($response['body']['collections']);
+ $this->assertIsArray($response['body']['documents']);
+ }
+
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollectionUsage(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for FAILURE
+ */
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '32h'
+ ]);
+
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/randomCollectionId/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+
+ $this->assertEquals(404, $response['headers']['status-code']);
+
+ /**
+ * Test for SUCCESS
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'range' => '24h'
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(3, count($response['body']));
+ $this->assertEquals('24h', $response['body']['range']);
+ $this->assertIsNumeric($response['body']['documentsTotal']);
+ $this->assertIsArray($response['body']['documents']);
+ }
+
+ #[Depends('testCreateCollection')]
+ public function testGetCollectionLogs(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ /**
+ * Test for SUCCESS
+ */
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::offset(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
+ ]);
+
+ $this->assertEquals(200, $logs['headers']['status-code']);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDBCustomClientTest.php b/tests/e2e/Services/Databases/VectorsDBCustomClientTest.php
new file mode 100644
index 0000000000..7add5c7f71
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDBCustomClientTest.php
@@ -0,0 +1,205 @@
+client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test Database'
+ ]);
+
+ $databaseId = $database['body']['$id'];
+
+ // Collection aliases write to create, update, delete
+ $movies = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $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',
+ 'dimension' => 3,
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::write(Role::user($this->getUser()['$id'])),
+ ],
+ ]);
+
+ $moviesId = $movies['body']['$id'];
+
+ $this->assertContains(Permission::create(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+ $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+ $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
+
+ // VectorsDB uses fixed schema (embeddings, metadata). No attribute creation needed.
+
+ // Document aliases write to update, delete
+ $document1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['k' => 'v'],
+ ],
+ 'permissions' => [
+ Permission::write(Role::user($this->getUser()['$id'])),
+ ]
+ ]);
+
+ $this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+ $this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+ $this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
+
+ /**
+ * Test for FAILURE
+ */
+
+ // Document does not allow create permission
+ $document2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['k' => 'v'],
+ ],
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ]
+ ]);
+
+ $this->assertEquals(400, $document2['headers']['status-code']);
+ }
+
+ public function testUpdateWithoutPermission(): array
+ {
+ // As a part of preparation, we get ID of currently logged-in user
+ $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $userId = $response['body']['$id'];
+
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::custom('permissionCheckDatabase'),
+ 'name' => 'Test Database',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $this->assertEquals('Test Database', $database['body']['name']);
+
+ $databaseId = $database['body']['$id'];
+ // Create collection
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::custom('permissionCheck'),
+ 'name' => 'permissionCheck',
+ 'dimension' => 3,
+ 'permissions' => [],
+ 'documentSecurity' => true,
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Creating document by server, give read permission to our user + some other user
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documentId' => ID::custom('permissionCheckDocument'),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['name' => 'AppwriteBeginner'],
+ ],
+ 'permissions' => [
+ Permission::read(Role::user(ID::custom('user2'))),
+ Permission::read(Role::user($userId)),
+ Permission::update(Role::user($userId)),
+ Permission::delete(Role::user($userId)),
+ ],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Update document
+ // This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
+ $response = $this->client->call(Client::METHOD_PATCH, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), [
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['name' => 'AppwriteExpert'],
+ ]
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Get name of the document, should be the new one
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals("AppwriteExpert", $response['body']['metadata']['name']);
+
+ // Cleanup to prevent collision with other tests
+ // Delete collection
+ $response = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/permissionCheck', 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']);
+
+
+ // Wait for database worker to finish deleting collection
+ sleep(2);
+
+ // Make sure collection has been deleted
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/permissionCheck', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+ $this->assertEquals(404, $response['headers']['status-code']);
+
+ return [];
+ }
+}
diff --git a/tests/e2e/Services/Databases/VectorsDBCustomServerTest.php b/tests/e2e/Services/Databases/VectorsDBCustomServerTest.php
new file mode 100644
index 0000000000..ceb672443e
--- /dev/null
+++ b/tests/e2e/Services/Databases/VectorsDBCustomServerTest.php
@@ -0,0 +1,963 @@
+client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::custom('first'),
+ 'name' => 'Test 1',
+ ]);
+ $this->assertEquals(201, $db1['headers']['status-code']);
+ $this->assertEquals('Test 1', $db1['body']['name']);
+ $this->assertEquals('vectorsdb', $db1['body']['type']);
+ // Validate database response model fields on create
+ $this->assertArrayHasKey('$id', $db1['body']);
+ $this->assertArrayHasKey('$createdAt', $db1['body']);
+ $this->assertArrayHasKey('$updatedAt', $db1['body']);
+ $this->assertArrayHasKey('enabled', $db1['body']);
+
+ $db2 = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::custom('second'),
+ 'name' => 'Test 2',
+ ]);
+ $this->assertEquals(201, $db2['headers']['status-code']);
+ $this->assertEquals('Test 2', $db2['body']['name']);
+ $this->assertEquals('vectorsdb', $db2['body']['type']);
+
+ $list = $this->client->call(Client::METHOD_GET, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsInt($list['body']['total']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+ $this->assertIsArray($list['body']['databases']);
+ $this->assertArrayHasKey('$id', $list['body']['databases'][0]);
+ $this->assertArrayHasKey('name', $list['body']['databases'][0]);
+ $this->assertArrayHasKey('type', $list['body']['databases'][0]);
+
+ return ['databaseId' => $db1['body']['$id']];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testGetDatabase(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $res = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals($databaseId, $res['body']['$id']);
+ $this->assertEquals('Test 1', $res['body']['name']);
+ $this->assertEquals('vectorsdb', $res['body']['type']);
+ return ['databaseId' => $databaseId];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testUpdateDatabase(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $res = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Updated',
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertEquals('Test 1 Updated', $res['body']['name']);
+ $this->assertEquals('vectorsdb', $res['body']['type']);
+ return ['databaseId' => $databaseId];
+ }
+
+ #[Depends('testListDatabases')]
+ public function testDeleteDatabase(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ $this->assertEquals("", $del['body']);
+
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $get['headers']['status-code']);
+ }
+
+ public function testCollectionsCRUD(): array
+ {
+ // Create database for collections tests
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Collections DB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ // Create two collections
+ $col1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1',
+ 'collectionId' => ID::custom('first'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ ]);
+ $this->assertEquals(201, $col1['headers']['status-code']);
+ // Validate collection response model on create
+ $this->assertArrayHasKey('$id', $col1['body']);
+ $this->assertArrayHasKey('$createdAt', $col1['body']);
+ $this->assertArrayHasKey('$updatedAt', $col1['body']);
+ $this->assertArrayHasKey('enabled', $col1['body']);
+ $this->assertArrayHasKey('documentSecurity', $col1['body']);
+ $this->assertArrayHasKey('dimension', $col1['body']);
+
+ $col2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 2',
+ 'collectionId' => ID::custom('second'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ ]);
+ $this->assertEquals(201, $col2['headers']['status-code']);
+ $this->assertArrayHasKey('$id', $col2['body']);
+ $this->assertArrayHasKey('$createdAt', $col2['body']);
+ $this->assertArrayHasKey('$updatedAt', $col2['body']);
+
+ // List collections
+ $list = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsInt($list['body']['total']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+ $this->assertIsArray($list['body']['collections']);
+ $this->assertArrayHasKey('$id', $list['body']['collections'][0]);
+ $this->assertArrayHasKey('name', $list['body']['collections'][0]);
+ $this->assertArrayHasKey('dimension', $list['body']['collections'][0]);
+
+ // Get collection
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($col1['body']['$id'], $get['body']['$id']);
+ $this->assertEquals('Test 1', $get['body']['name']);
+ $this->assertEquals(3, $get['body']['dimension']);
+
+ // Update collection (name only)
+ $upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Updated',
+ ]);
+ $this->assertEquals(200, $upd['headers']['status-code']);
+ $this->assertEquals('Test 1 Updated', $upd['body']['name']);
+ $this->assertArrayHasKey('$updatedAt', $upd['body']);
+
+ // Delete collection
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $col2['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ $this->assertEquals("", $del['body']);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $col1['body']['$id'],
+ ];
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testUpdateCollectionMore(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Update collection name and dimensions
+ $upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test 1 Renamed',
+ 'dimension' => 4,
+ ]);
+ $this->assertEquals(200, $upd['headers']['status-code']);
+ $this->assertEquals('Test 1 Renamed', $upd['body']['name']);
+ $this->assertEquals(4, $upd['body']['dimension']);
+
+ // Read back to confirm
+ $get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('Test 1 Renamed', $get['body']['name']);
+ $this->assertEquals(4, $get['body']['dimension']);
+
+ return $data;
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testUpdateCollectionEnabledFlag(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Disable collection
+ $disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Updated',
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $disable['headers']['status-code']);
+ $this->assertFalse($disable['body']['enabled']);
+
+ // Re-enable collection
+ $enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Updated',
+ 'enabled' => true,
+ ]);
+ $this->assertEquals(200, $enable['headers']['status-code']);
+ $this->assertTrue($enable['body']['enabled']);
+
+ return $data;
+ }
+
+ public function testUpdateDatabaseNameAndEnabled(): void
+ {
+ // Create isolated database for this test to avoid ordering conflicts
+ $create = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Update DB',
+ ]);
+ $this->assertEquals(201, $create['headers']['status-code']);
+ $databaseId = $create['body']['$id'];
+
+ // Update name
+ $rename = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ ]);
+ $this->assertEquals(200, $rename['headers']['status-code']);
+ $this->assertEquals('Test DB Renamed', $rename['body']['name']);
+
+ // Toggle enabled off then on
+ $disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $disable['headers']['status-code']);
+ $this->assertFalse($disable['body']['enabled']);
+
+ $enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'name' => 'Test DB Renamed',
+ 'enabled' => true,
+ ]);
+ $this->assertEquals(200, $enable['headers']['status-code']);
+ $this->assertTrue($enable['body']['enabled']);
+
+ // Cleanup
+ $del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testRecreateIndex(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create a new index variant
+ $create = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_euclidean_v2',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $create['headers']['status-code']);
+
+ // Ensure it exists
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('embedding_euclidean_v2', $get['body']['key']);
+
+ // Delete it
+ $del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ }
+
+ #[Depends('testCollectionsCRUD')]
+ public function testIndexesCRUD(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ // Create indexes
+ $eu = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_euclidean',
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $eu['headers']['status-code']);
+
+ $dot = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_dot',
+ 'type' => Database::INDEX_HNSW_DOT,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $dot['headers']['status-code']);
+
+ $cos = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'embedding_cosine',
+ 'type' => Database::INDEX_HNSW_COSINE,
+ 'attributes' => ['embeddings']
+ ]);
+ $this->assertEquals(202, $cos['headers']['status-code']);
+
+ // List indexes
+ $list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $list['headers']['status-code']);
+ $this->assertIsArray($list['body']['indexes']);
+ $keys = array_map(fn ($i) => $i['key'], $list['body']['indexes']);
+ $this->assertContains('embedding_euclidean', $keys);
+ $this->assertContains('embedding_dot', $keys);
+ $this->assertContains('embedding_cosine', $keys);
+
+ // Get index by key
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals('embedding_euclidean', $get['body']['key']);
+ $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $get['body']['type']);
+
+ // Delete index
+ $del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(204, $del['headers']['status-code']);
+ sleep(4);
+ // Ensure it's gone
+ $getMissing = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $getMissing['headers']['status-code']);
+ }
+
+ public function testBulkCreate(): array
+ {
+ // Setup: create isolated database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'BulkDBCreate'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColCreate',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $docs = [
+ [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['group' => 'bulkA'],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['group' => 'bulkB'],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ ];
+
+ $res = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs
+ ]);
+
+ $this->assertEquals(201, $res['headers']['status-code']);
+ $this->assertIsInt($res['body']['total'] ?? 0);
+ $this->assertGreaterThanOrEqual(2, $res['body']['total']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+
+ $ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
+ $this->assertNotEmpty($ids[0]);
+ $this->assertNotEmpty($ids[1]);
+
+ // Fetch and validate persisted data via GET
+ foreach ($ids as $i => $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertEquals($id, $get['body']['$id']);
+ $this->assertIsArray($get['body']['embeddings']);
+ $this->assertCount(3, $get['body']['embeddings']);
+ $this->assertArrayHasKey('group', $get['body']['metadata']);
+ }
+
+ return [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'bulkIds' => $ids ];
+ }
+
+ public function testCreateTextEmbeddingsSuccessAndErrors(): void
+ {
+ // Setup new database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'EmbedDB',
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'EmbedCol',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Success: two embeddings
+ $this->assertEventually(function () {
+ $ok = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'embeddinggemma',
+ 'texts' => [
+ 'hello world',
+ 'second sentence',
+ ],
+ ]);
+ $this->assertEquals(200, $ok['headers']['status-code']);
+ $this->assertIsInt($ok['body']['total'] ?? 0);
+ $this->assertEquals(2, $ok['body']['total']);
+ $this->assertIsArray($ok['body']['embeddings']);
+ $this->assertCount(2, $ok['body']['embeddings']);
+ foreach ($ok['body']['embeddings'] as $embed) {
+ $this->assertIsString($embed['model']);
+ $this->assertIsInt($embed['dimension']);
+ $this->assertIsArray($embed['embedding']);
+ $this->assertGreaterThan(0, count($embed['embedding']));
+ $this->assertArrayHasKey('error', $embed);
+ }
+ }, 3000, 100);
+
+ // Error: missing texts payload
+ $missingTexts = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], []);
+ $this->assertEquals(400, $missingTexts['headers']['status-code']);
+
+ // Error: invalid texts item type (must be strings)
+ $invalidItem = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'embeddinggemma',
+ 'texts' => [
+ 'valid text',
+ 123, // invalid, not a string
+ ],
+ ]);
+ $this->assertEquals(400, $invalidItem['headers']['status-code']);
+
+ // Error: unknown embedding model
+ $unknownModel = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'model' => 'nonexistent-model',
+ 'texts' => ['hello'],
+ ]);
+ $this->assertEquals(400, $unknownModel['headers']['status-code']);
+ }
+
+ public function testBulkUpsert(): void
+ {
+ // Setup fresh db/collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpsert' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColUpsert',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $docs = [
+ [
+ 'embeddings' => [0.5, 0.5, 0.0],
+ 'metadata' => ['group' => 'bulkA', 'updated' => true],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ [
+ 'embeddings' => [0.2, 0.8, 0.0],
+ 'metadata' => ['group' => 'bulkB', 'updated' => true],
+ '$permissions' => [Permission::read(Role::any())]
+ ],
+ ];
+
+ $res = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs
+ ]);
+
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+ $this->assertTrue($res['body']['documents'][0]['metadata']['updated']);
+ $this->assertTrue($res['body']['documents'][1]['metadata']['updated']);
+
+ // Fetch and validate updated content
+ $ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertTrue($get['body']['metadata']['updated']);
+ }
+
+ // Perform another bulk upsert to mutate the same documents
+ $docs2 = [
+ [ 'embeddings' => [0.6, 0.4, 0.0], 'metadata' => ['updatedAgain' => true] ],
+ [ 'embeddings' => [0.3, 0.7, 0.0], 'metadata' => ['updatedAgain' => true] ],
+ ];
+ $res2 = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => $docs2
+ ]);
+ $this->assertEquals(200, $res2['headers']['status-code']);
+ $this->assertIsArray($res2['body']['documents']);
+ $this->assertCount(2, $res2['body']['documents']);
+
+ // Fetch again and assert second update persisted
+ $ids2 = array_map(fn ($d) => $d['$id'], $res2['body']['documents']);
+ foreach ($ids2 as $id) {
+ $get2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get2['headers']['status-code']);
+ $this->assertTrue($get2['body']['metadata']['updatedAgain']);
+ }
+ }
+
+ public function testBulkUpdate(): void
+ {
+ // Setup: create db/collection and two docs
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpdate' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColUpdate',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => [
+ ['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
+ ['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
+ ]
+ ]);
+ $this->assertEquals(200, $seed['headers']['status-code']);
+ $ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
+
+ $res = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [ 'metadata' => ['bulkUpdated' => true] ],
+ 'queries' => [
+ \Utopia\Database\Query::equal('$id', $ids)->toString()
+ ]
+ ]);
+
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsArray($res['body']['documents']);
+ $this->assertCount(2, $res['body']['documents']);
+ foreach ($res['body']['documents'] as $doc) {
+ $this->assertTrue($doc['metadata']['bulkUpdated']);
+ }
+
+ // Fetch by IDs and assert update persisted
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(200, $get['headers']['status-code']);
+ $this->assertTrue($get['body']['metadata']['bulkUpdated']);
+ }
+ }
+
+ public function testBulkDelete(): void
+ {
+ // Setup: create db/collection and two docs
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBDelete' ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'BulkColDelete',
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ $seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documents' => [
+ ['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
+ ['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
+ ]
+ ]);
+ $this->assertEquals(200, $seed['headers']['status-code']);
+ $ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
+
+ $res = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'queries' => [
+ \Utopia\Database\Query::equal('$id', $ids)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $res['headers']['status-code']);
+ $this->assertIsInt($res['body']['total'] ?? 0);
+ $this->assertGreaterThanOrEqual(2, $res['body']['total']);
+
+ // Ensure they are deleted
+ foreach ($ids as $id) {
+ $get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+ $this->assertEquals(404, $get['headers']['status-code']);
+ }
+ }
+
+ public function testCustomTimestamps(): void
+ {
+ // Setup: create database and collection
+ $db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'TimestampTestDB'
+ ]);
+ $this->assertEquals(201, $db['headers']['status-code']);
+ $databaseId = $db['body']['$id'];
+
+ $col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'TimestampTestCollection',
+ 'documentSecurity' => true,
+ 'dimension' => 1536,
+ 'permissions' => [Permission::read(Role::any())]
+ ]);
+ $this->assertEquals(201, $col['headers']['status-code']);
+ $collectionId = $col['body']['$id'];
+
+ // Test: Create document with custom timestamps using PUT (upsert)
+ $customCreatedAt = '1970-01-01T00:00:00.000+00:00';
+ $customUpdatedAt = '1970-01-01T00:00:00.000+00:00';
+ $vector = array_fill(0, 1536, 0.0);
+ $vector[0] = 1.0;
+ $documentId = ID::unique();
+
+ $doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'documentId' => $documentId,
+ 'data' => [
+ '$createdAt' => $customCreatedAt,
+ '$updatedAt' => $customUpdatedAt,
+ 'embeddings' => $vector,
+ 'metadata' => ['test' => 'custom_timestamps']
+ ]
+ ]);
+
+ $this->assertEquals(201, $doc['headers']['status-code']);
+ $documentId = $doc['body']['$id'];
+ $this->assertNotEmpty($documentId);
+
+ // Verify timestamps were set correctly
+ $this->assertEquals($customCreatedAt, $doc['body']['$createdAt'], 'CreatedAt should match custom timestamp');
+ $this->assertEquals($customUpdatedAt, $doc['body']['$updatedAt'], 'UpdatedAt should match custom timestamp');
+
+ // Fetch document and verify timestamps persist
+ $fetched = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $fetched['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $fetched['body']['$createdAt'], 'CreatedAt should persist after fetch');
+ $this->assertEquals($customUpdatedAt, $fetched['body']['$updatedAt'], 'UpdatedAt should persist after fetch');
+
+ // Test: Update document with new custom timestamps
+ $newCustomUpdatedAt = '2000-01-01T12:00:00.000+00:00';
+ $vector2 = array_fill(0, 1536, 0.0);
+ $vector2[1] = 1.0;
+
+ $updated = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'data' => [
+ '$createdAt' => $customCreatedAt, // Keep original createdAt
+ '$updatedAt' => $newCustomUpdatedAt, // Update updatedAt
+ 'embeddings' => $vector2,
+ 'metadata' => ['test' => 'updated_timestamps']
+ ]
+ ]);
+
+ $this->assertEquals(200, $updated['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $updated['body']['$createdAt'], 'CreatedAt should remain unchanged');
+ $this->assertEquals($newCustomUpdatedAt, $updated['body']['$updatedAt'], 'UpdatedAt should be updated to new custom timestamp');
+
+ // Final verification
+ $final = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]);
+
+ $this->assertEquals(200, $final['headers']['status-code']);
+ $this->assertEquals($customCreatedAt, $final['body']['$createdAt'], 'CreatedAt should persist through updates');
+ $this->assertEquals($newCustomUpdatedAt, $final['body']['$updatedAt'], 'UpdatedAt should reflect the latest custom timestamp');
+ }
+
+}
diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php
index 77c9367c44..af426d5221 100644
--- a/tests/e2e/Services/Functions/FunctionsBase.php
+++ b/tests/e2e/Services/Functions/FunctionsBase.php
@@ -264,7 +264,7 @@ trait FunctionsBase
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index 508ddede4a..d0b2190f1c 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -991,7 +991,7 @@ class FunctionsCustomServerTest extends Scope
*/
$folder = 'large';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
- Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
+ Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
$chunkSize = 5 * 1024 * 1024;
$handle = @fopen($code, "rb");
diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php
index 3e2624f83c..c42679018e 100644
--- a/tests/e2e/Services/GraphQL/Base.php
+++ b/tests/e2e/Services/GraphQL/Base.php
@@ -3464,7 +3464,7 @@ trait Base
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php
index 0d992c472e..1e8b1f5ad3 100644
--- a/tests/e2e/Services/Migrations/MigrationsBase.php
+++ b/tests/e2e/Services/Migrations/MigrationsBase.php
@@ -2,12 +2,15 @@
namespace Tests\E2E\Services\Migrations;
+use Appwrite\Tests\Retry;
use CURLFile;
+use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\General\UsageTest;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Services\Functions\FunctionsBase;
use Utopia\Console;
+use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -186,6 +189,26 @@ trait MigrationsBase
return $migrationResult;
}
+ /**
+ * Get migration status by ID (without creating a new migration)
+ *
+ * @param string $migrationId
+ * @return array
+ */
+ public function getMigrationStatus(string $migrationId): array
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+
+ return $response['body'];
+ }
+
/**
* Appwrite E2E Migration Tests
*/
@@ -1165,7 +1188,7 @@ trait MigrationsBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
@@ -2532,4 +2555,1274 @@ trait MigrationsBase
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
+
+ /**
+ * Import VectorsDB documents from CSV
+ */
+ public function testImportVectordbCSV(): void
+ {
+ $databaseId = null;
+ $collectionId = null;
+ $bucketId = null;
+
+ try {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Vector CSV Import DB'
+ ]);
+
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Vector CSV Import Collection',
+ 'dimension' => 3,
+ 'documentSecurity' => true,
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'bucketId' => ID::unique(),
+ 'name' => 'Vector CSV Bucket',
+ 'maximumFileSize' => 2000000,
+ 'allowedFileExtensions' => ['csv'],
+ ]);
+
+ $this->assertEquals(201, $bucket['headers']['status-code']);
+ $bucketId = $bucket['body']['$id'];
+
+ $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', [
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'fileId' => ID::unique(),
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/csv/vectorsdb-documents.csv'), 'text/csv', 'vectorsdb-documents.csv'),
+ ]);
+
+ $this->assertEquals(201, $file['headers']['status-code']);
+ $fileId = $file['body']['$id'];
+
+ $migration = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $collectionId,
+ ]);
+
+ $this->assertEquals(202, $migration['headers']['status-code']);
+
+ $this->assertEventually(function () use ($migration) {
+ $migrationId = $migration['body']['$id'];
+ $status = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $status['headers']['status-code']);
+ $this->assertEquals('finished', $status['body']['stage']);
+ $this->assertEquals('completed', $status['body']['status']);
+ $this->assertContains(Resource::TYPE_DOCUMENT, $status['body']['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_DOCUMENT, $status['body']['statusCounters']);
+ $this->assertEquals(2, $status['body']['statusCounters'][Resource::TYPE_DOCUMENT]['success']);
+
+ return true;
+ }, 60_000, 500);
+
+ $documents = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => [
+ Query::limit(10)->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $documents['headers']['status-code']);
+ $this->assertEquals(2, $documents['body']['total']);
+
+ $titles = array_map(fn ($doc) => $doc['metadata']['title'] ?? null, $documents['body']['documents']);
+ $this->assertContains('Vector Alpha', $titles);
+ $this->assertContains('Vector Beta', $titles);
+ } finally {
+ if ($bucketId) {
+ $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+
+ if ($databaseId) {
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+ }
+ }
+
+ /**
+ * Export VectorsDB documents to CSV
+ */
+ #[Retry(count: 1)]
+ public function testExportVectordbCSV(): void
+ {
+ $databaseId = null;
+
+ try {
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Vector CSV Export DB',
+ ]);
+
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $collectionId = null;
+ $this->assertEventually(function () use ($databaseId, &$collectionId) {
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Vector CSV Export Collection',
+ 'dimension' => 3,
+ 'documentSecurity' => true,
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+ });
+
+ $documentsPayload = [
+ [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.11, 0.22, 0.33],
+ 'metadata' => ['title' => 'Vector Sample One', 'category' => 'alpha'],
+ ],
+ ],
+ [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.44, 0.55, 0.66],
+ 'metadata' => ['title' => 'Vector Sample Two', 'category' => 'beta'],
+ ],
+ ],
+ ];
+
+ foreach ($documentsPayload as $payload) {
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $payload);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ }
+
+ $filename = 'vectorsdb-export-' . ID::unique();
+ $migration = $this->client->call(Client::METHOD_POST, '/migrations/csv/exports', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'resourceId' => $databaseId . ':' . $collectionId,
+ 'filename' => $filename,
+ 'columns' => [],
+ 'queries' => [],
+ 'delimiter' => ',',
+ 'enclosure' => '"',
+ 'escape' => '\\',
+ 'header' => true,
+ 'notify' => true,
+ ]);
+
+ $this->assertEquals(202, $migration['headers']['status-code']);
+
+ $migrationId = $migration['body']['$id'];
+ $this->assertEventually(function () use ($migrationId) {
+ $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('finished', $response['body']['stage']);
+ $this->assertEquals('completed', $response['body']['status']);
+
+ return true;
+ }, 30_000, 500);
+
+ $this->assertEventually(function () {
+ $email = $this->getLastEmail(1, function (array $email) {
+ $this->assertEquals('Your CSV export is ready', $email['subject']);
+ });
+ $this->assertNotEmpty($email);
+ $this->assertEquals('Your CSV export is ready', $email['subject']);
+ \preg_match('/href="([^"]*\/storage\/buckets\/[^"]*\/push[^"]*)"/', $email['html'], $matches);
+ $this->assertNotEmpty($matches[1], 'Download URL not found in email');
+ $downloadUrl = html_entity_decode($matches[1]);
+ $components = \parse_url($downloadUrl);
+ $this->assertNotEmpty($components);
+ \parse_str($components['query'] ?? '', $queryParams);
+ $this->assertArrayHasKey('jwt', $queryParams);
+ $this->assertArrayHasKey('project', $queryParams);
+
+ $path = \str_replace('/v1', '', $components['path']);
+ $downloadResponse = $this->client->call(Client::METHOD_GET, $path . '?project=' . $queryParams['project'] . '&jwt=' . $queryParams['jwt']);
+ $this->assertEquals(200, $downloadResponse['headers']['status-code']);
+
+ $csvData = $downloadResponse['body'];
+ $this->assertStringContainsString('Vector Sample One', $csvData);
+ $this->assertStringContainsString('Vector Sample Two', $csvData);
+ $this->assertStringContainsString('[0.11,0.22,0.33]', $csvData);
+ }, 30_000, 500);
+ } finally {
+ if ($databaseId) {
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+ }
+ }
+
+ /**
+ * DocumentsDB (schemaless)
+ */
+ public function testAppwriteMigrationDocumentsDBDatabase(): array
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/documentsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'DocsDB - Migration DB'
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $databaseId = $response['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_DOCUMENTSDB,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_DATABASE_DOCUMENTSDB], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE_DOCUMENTSDB, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals($databaseId, $response['body']['$id']);
+ $this->assertEquals('DocsDB - Migration DB', $response['body']['name']);
+
+ // Cleanup on destination
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ ];
+ }
+
+ /**
+ * VectorsDB (embeddings collections)
+ */
+ public function testAppwriteMigrationVectorsDBDatabase(): array
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'VDB - Migration DB'
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $databaseId = $response['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_VECTORSDB,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_DATABASE_VECTORSDB], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE_VECTORSDB, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['error'] ?? 0);
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals($databaseId, $response['body']['$id']);
+ $this->assertEquals('VDB - Migration DB', $response['body']['name']);
+
+ // Cleanup on destination
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ ];
+ }
+
+ #[Depends('testAppwriteMigrationVectorsDBDatabase')]
+ public function testAppwriteMigrationVectorsDBCollection(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'VDB - Movies',
+ 'dimension' => 3,
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+
+ $collectionId = $collection['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_VECTORSDB,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_ATTRIBUTE,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $result['status']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertEquals($collectionId, $response['body']['$id']);
+ $this->assertEquals('VDB - Movies', $response['body']['name']);
+ // Verify attributes are present (embeddings and metadata are default attributes)
+ $this->assertArrayHasKey('attributes', $response['body']);
+ $this->assertIsArray($response['body']['attributes']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ ];
+ }
+
+ #[Depends('testAppwriteMigrationVectorsDBCollection')]
+ public function testAppwriteMigrationVectorsDBDocument(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $document = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['title' => 'Migration Test Movie'],
+ ]
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $documentId = $document['body']['$id'];
+
+ // Ensure attributes are exported before documents
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_VECTORSDB,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_ATTRIBUTE,
+ Resource::TYPE_DOCUMENT,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ // Verify that TYPE_ATTRIBUTE appears in the resources array for VectorsDB
+ $this->assertContains(Resource::TYPE_ATTRIBUTE, $result['resources'], 'TYPE_ATTRIBUTE should be in resources array for VectorsDB');
+
+ // Verify attributes exist on destination before checking document
+ $collectionResponse = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $collectionResponse['headers']['status-code']);
+ $this->assertArrayHasKey('attributes', $collectionResponse['body']);
+ $this->assertIsArray($collectionResponse['body']['attributes']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertEquals($documentId, $response['body']['$id']);
+ $this->assertEquals('Migration Test Movie', $response['body']['metadata']['title']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+
+ #[Depends('testAppwriteMigrationDocumentsDBDatabase')]
+ public function testAppwriteMigrationDocumentsDBCollection(array $data): array
+ {
+ $databaseId = $data['databaseId'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'DocsDB - Movies',
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+
+ $collectionId = $collection['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_DOCUMENTSDB,
+ Resource::TYPE_COLLECTION, // collections in DocumentsDB map to tables in migration
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $result['status']);
+ foreach ([Resource::TYPE_DATABASE_DOCUMENTSDB, Resource::TYPE_COLLECTION] as $resource) {
+ $this->assertArrayHasKey($resource, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['error']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][$resource]['success']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
+ }
+
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertEquals($collectionId, $response['body']['$id']);
+ $this->assertEquals('DocsDB - Movies', $response['body']['name']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ ];
+ }
+
+ #[Depends('testAppwriteMigrationDocumentsDBCollection')]
+ public function testAppwriteMigrationDocumentsDBDocument(array $data): void
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $document = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'title' => 'Migration Test Movie',
+ 'releaseYear' => 1999,
+ ]
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $documentId = $document['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE_DOCUMENTSDB,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_DOCUMENT,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+
+ foreach ([Resource::TYPE_DATABASE_DOCUMENTSDB] as $resource) {
+ $this->assertArrayHasKey($resource, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['error']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][$resource]['success']);
+ }
+
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertEquals($documentId, $response['body']['$id']);
+ $this->assertEquals('Migration Test Movie', $response['body']['title']);
+ $this->assertEquals(1999, $response['body']['releaseYear']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+
+ /**
+ * Migrate a project that contains both SQL Databases (/databases) and
+ * schemaless DocumentsDB (/documentsdb) in a single run and verify results.
+ * Uses a dedicated isolated source project to avoid interference from other tests.
+ */
+ public function testAppwriteMigrationMixedDatabases(): void
+ {
+ // Create a fresh isolated source project for this test
+ $sourceProject = $this->getProject(true);
+
+ // ====== Create SQL Database (/databases) with table, column, and row ======
+ $sql = $this->client->call(Client::METHOD_POST, '/databases', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Mixed SQL DB',
+ ]);
+
+ $this->assertEquals(201, $sql['headers']['status-code']);
+ $this->assertNotEmpty($sql['body']['$id']);
+ $sqlDatabaseId = $sql['body']['$id'];
+
+ // Create Table in SQL Database
+ $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'tableId' => ID::unique(),
+ 'name' => 'Products',
+ ]);
+
+ $this->assertEquals(201, $table['headers']['status-code']);
+ $tableId = $table['body']['$id'];
+
+ // Create Column in Table
+ $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/columns/string', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'key' => 'productName',
+ 'size' => 255,
+ 'required' => true,
+ ]);
+
+ $this->assertEquals(202, $column['headers']['status-code']);
+
+ // Wait for column to be ready
+ $this->assertEventually(function () use ($sqlDatabaseId, $tableId, $sourceProject) {
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/columns/productName', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('available', $response['body']['status']);
+ }, 5000, 500);
+
+ $sqlIndexKey = 'product_unique';
+
+ $sqlIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'key' => $sqlIndexKey,
+ 'type' => Database::INDEX_UNIQUE,
+ 'columns' => ['productName'],
+ ]);
+
+ $this->assertEquals(202, $sqlIndex['headers']['status-code']);
+
+ $this->assertEventually(function () use ($sqlDatabaseId, $tableId, $sqlIndexKey, $sourceProject) {
+ $index = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes/' . $sqlIndexKey, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $index['headers']['status-code']);
+ $this->assertEquals('available', $index['body']['status']);
+ }, 30000, 500);
+
+ // Create Row in Table
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/rows', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'rowId' => ID::unique(),
+ 'data' => [
+ 'productName' => 'Laptop',
+ ],
+ ]);
+
+ $this->assertEquals(201, $row['headers']['status-code']);
+ $rowId = $row['body']['$id'];
+
+ // ====== Create DocumentsDB (/documentsdb) with collection and document ======
+ $docs = $this->client->call(Client::METHOD_POST, '/documentsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Mixed DocsDB',
+ ]);
+
+ $this->assertEquals(201, $docs['headers']['status-code']);
+ $this->assertNotEmpty($docs['body']['$id']);
+ $docsDatabaseId = $docs['body']['$id'];
+
+ // Create Collection in DocumentsDB
+ $collection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $docsDatabaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Users',
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+ $collectionId = $collection['body']['$id'];
+
+ $documentsIndexKey = 'email_unique';
+
+ $documentsIndex = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'key' => $documentsIndexKey,
+ 'type' => Database::INDEX_UNIQUE,
+ 'attributes' => ['email'],
+ ]);
+
+ $this->assertEquals(202, $documentsIndex['headers']['status-code']);
+
+ $this->assertEventually(function () use ($docsDatabaseId, $collectionId, $documentsIndexKey, $sourceProject) {
+ $index = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes/' . $documentsIndexKey, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $index['headers']['status-code']);
+ $this->assertEquals('available', $index['body']['status']);
+ }, 30000, 500);
+
+ // Create Document in Collection
+ $document = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+ ],
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $documentId = $document['body']['$id'];
+
+ // ====== Create VectorsDB (/vectorsdb) with collection and document ======
+ $vector = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Mixed VectorsDB',
+ ]);
+
+ $this->assertEquals(201, $vector['headers']['status-code']);
+ $this->assertNotEmpty($vector['body']['$id']);
+ $vectorDatabaseId = $vector['body']['$id'];
+
+ // Create Collection in VectorsDB
+ $vectorCollection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $vectorDatabaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Products',
+ 'dimension' => 3,
+ ]);
+
+ $this->assertEquals(201, $vectorCollection['headers']['status-code']);
+ $vectorCollectionId = $vectorCollection['body']['$id'];
+
+ // Wait for VectorsDB collection attributes to be ready
+ $this->assertEventually(function () use ($vectorDatabaseId, $vectorCollectionId, $sourceProject) {
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertArrayHasKey('attributes', $response['body']);
+ $this->assertIsArray($response['body']['attributes']);
+ // Check that default attributes (embeddings and metadata) are present and ready
+ $attributeKeys = array_column($response['body']['attributes'], 'key');
+ $this->assertContains('embeddings', $attributeKeys);
+ $this->assertContains('metadata', $attributeKeys);
+ // Check that attributes are available (if status field exists)
+ foreach ($response['body']['attributes'] as $attribute) {
+ if (isset($attribute['status']) && $attribute['status'] !== 'available') {
+ return false;
+ }
+ }
+ return true;
+ }, 10000, 500);
+
+ $metadataIndexKey = '_key_metadata';
+ $vectorIndexes = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+ $this->assertEquals(200, $vectorIndexes['headers']['status-code']);
+ $metadataIndex = null;
+ foreach ($vectorIndexes['body']['indexes'] ?? [] as $index) {
+ if (($index['key'] ?? '') === $metadataIndexKey) {
+ $metadataIndex = $index;
+ break;
+ }
+ }
+ $this->assertNotNull($metadataIndex, 'Default metadata index should exist on source collection');
+ $this->assertEquals(Database::INDEX_OBJECT, $metadataIndex['type']);
+
+ $vectorEmbeddingIndexKey = 'embedding_euclidean';
+ $vectorEmbeddingIndex = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'key' => $vectorEmbeddingIndexKey,
+ 'type' => Database::INDEX_HNSW_EUCLIDEAN,
+ 'attributes' => ['embeddings'],
+ ]);
+ $this->assertEquals(202, $vectorEmbeddingIndex['headers']['status-code']);
+
+ $this->assertEventually(function () use ($vectorDatabaseId, $vectorCollectionId, $vectorEmbeddingIndexKey, $sourceProject) {
+ $index = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes/' . $vectorEmbeddingIndexKey, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $index['headers']['status-code']);
+ $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $index['body']['type']);
+ if (isset($index['body']['status'])) {
+ $this->assertEquals('available', $index['body']['status']);
+ }
+ }, 30000, 500);
+
+ // Create Document in VectorsDB Collection
+ $vectorDocument = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [0.5, 0.3, 0.2],
+ 'metadata' => ['name' => 'Product Vector'],
+ ],
+ ]);
+
+ $this->assertEquals(201, $vectorDocument['headers']['status-code']);
+ $vectorDocumentId = $vectorDocument['body']['$id'];
+
+ // ====== Perform migration including all three database kinds with all child resources ======
+ $migrationConfig = [
+ 'resources' => [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ Resource::TYPE_DATABASE_DOCUMENTSDB,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_DOCUMENT,
+ Resource::TYPE_DATABASE_VECTORSDB,
+ Resource::TYPE_ATTRIBUTE,
+ Resource::TYPE_INDEX,
+ ],
+ 'endpoint' => $this->endpoint,
+ 'projectId' => $sourceProject['$id'],
+ 'apiKey' => $sourceProject['apiKey'],
+ ];
+
+ // Perform migration sync once and get migration ID
+ $result = $this->performMigrationSync($migrationConfig);
+ $migrationId = $result['$id'];
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals('Appwrite', $result['source']);
+ $this->assertEquals('Appwrite', $result['destination']);
+ $this->assertEquals([
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ Resource::TYPE_DATABASE_DOCUMENTSDB,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_DOCUMENT,
+ Resource::TYPE_DATABASE_VECTORSDB,
+ Resource::TYPE_ATTRIBUTE,
+ Resource::TYPE_INDEX,
+ ], $result['resources']);
+
+ // Get migration status before asserting SQL Database counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert SQL Database counters
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['warning']);
+
+ // Get migration status before asserting Table counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert Table counters
+ $this->assertArrayHasKey(Resource::TYPE_TABLE, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_TABLE]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['warning']);
+
+ // Get migration status before asserting Column counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert Column counters
+ $this->assertArrayHasKey(Resource::TYPE_COLUMN, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_COLUMN]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['warning']);
+
+ // Get migration status before asserting Row counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert Row counters
+ $this->assertArrayHasKey(Resource::TYPE_ROW, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_ROW]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['warning']);
+
+ // Get migration status before asserting DocumentsDB counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert DocumentsDB counters
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE_DOCUMENTSDB, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['warning']);
+
+ // Wait for all collections to be fully processed and status counters to be updated
+ // Note: Collections are being transferred but status counters may not be updated immediately
+ // This wait ensures the migration worker has finished processing all collections
+ $result = null;
+ $this->assertEventually(function () use ($migrationId, &$result) {
+ $result = $this->getMigrationStatus($migrationId);
+
+ // Check if collections status counters exist
+ if (!isset($result['statusCounters'][Resource::TYPE_COLLECTION])) {
+ return false;
+ }
+
+ $pendingCount = $result['statusCounters'][Resource::TYPE_COLLECTION]['pending'] ?? 0;
+
+ // Return true only when pending count is 0
+ return $pendingCount === 0;
+ }, 30000, 1000); // 30 second timeout, check every 1 second
+
+ // Assert Collection counters (covers both DocumentsDB and VectorsDB collections)
+ $this->assertArrayHasKey(Resource::TYPE_COLLECTION, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['pending']);
+ $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_COLLECTION]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['warning']);
+
+ // Get migration status before asserting Document counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert Document counters (covers both DocumentsDB and VectorsDB documents)
+ $this->assertArrayHasKey(Resource::TYPE_DOCUMENT, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['pending']);
+ $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_DOCUMENT]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['warning']);
+
+ // Get migration status before asserting VectorsDB counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert VectorsDB counters
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE_VECTORSDB, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORSDB]['warning']);
+
+ // Get migration status before asserting Attribute counters
+ $result = $this->getMigrationStatus($migrationId);
+ // Assert Attribute counters (for VectorsDB)
+ $this->assertArrayHasKey(Resource::TYPE_ATTRIBUTE, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['pending']);
+ $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['warning']);
+
+ // Get migration status before asserting Index counters
+ $result = $this->getMigrationStatus($migrationId);
+ $this->assertArrayHasKey(Resource::TYPE_INDEX, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['pending']);
+ $this->assertGreaterThanOrEqual(4, $result['statusCounters'][Resource::TYPE_INDEX]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['warning']);
+
+ // Get migration status before asserting counter count
+ $result = $this->getMigrationStatus($migrationId);
+ // Ensure only expected counters exist (10 total)
+ $this->assertCount(10, $result['statusCounters']);
+
+ // ====== Validate on destination: SQL Database resources ======
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $sqlDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($sqlDatabaseId, $response['body']['$id']);
+ $this->assertEquals('Mixed SQL DB', $response['body']['name']);
+
+ // Validate Table
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($tableId, $response['body']['$id']);
+ $this->assertEquals('Products', $response['body']['name']);
+
+ // Validate Column
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/columns/productName', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('productName', $response['body']['key']);
+ $this->assertEquals(255, $response['body']['size']);
+ $this->assertEquals(true, $response['body']['required']);
+
+ // Validate Row
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/rows/' . $rowId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($rowId, $response['body']['$id']);
+ $this->assertEquals('Laptop', $response['body']['productName']);
+
+ $sqlIndexDestination = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes/' . $sqlIndexKey, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $sqlIndexDestination['headers']['status-code']);
+ $this->assertEquals($sqlIndexKey, $sqlIndexDestination['body']['key']);
+ $this->assertEquals(Database::INDEX_UNIQUE, $sqlIndexDestination['body']['type']);
+ if (isset($sqlIndexDestination['body']['columns'])) {
+ $this->assertEquals(['productName'], $sqlIndexDestination['body']['columns']);
+ }
+
+ // ====== Validate on destination: DocumentsDB resources ======
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($docsDatabaseId, $response['body']['$id']);
+ $this->assertEquals('Mixed DocsDB', $response['body']['name']);
+
+ // Validate Collection
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($collectionId, $response['body']['$id']);
+ $this->assertEquals('Users', $response['body']['name']);
+
+ // Validate Document
+ $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($documentId, $response['body']['$id']);
+ $this->assertEquals('John Doe', $response['body']['name']);
+ $this->assertEquals('john@example.com', $response['body']['email']);
+
+ $documentsIndexDestination = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes/' . $documentsIndexKey, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $documentsIndexDestination['headers']['status-code']);
+ $this->assertEquals($documentsIndexKey, $documentsIndexDestination['body']['key']);
+ $this->assertEquals(Database::INDEX_UNIQUE, $documentsIndexDestination['body']['type']);
+ if (isset($documentsIndexDestination['body']['attributes'])) {
+ $this->assertEquals(['email'], $documentsIndexDestination['body']['attributes']);
+ }
+
+ // ====== Validate on destination: VectorsDB resources ======
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($vectorDatabaseId, $response['body']['$id']);
+ $this->assertEquals('Mixed VectorsDB', $response['body']['name']);
+
+ // Validate VectorsDB Collection
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($vectorCollectionId, $response['body']['$id']);
+ $this->assertEquals('Products', $response['body']['name']);
+ // Verify attributes are present (embeddings and metadata are default attributes)
+ $this->assertArrayHasKey('attributes', $response['body']);
+ $this->assertIsArray($response['body']['attributes']);
+
+ $vectorIndexesDestination = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $vectorIndexesDestination['headers']['status-code']);
+ $indexByKey = [];
+ foreach ($vectorIndexesDestination['body']['indexes'] ?? [] as $index) {
+ if (isset($index['key'])) {
+ $indexByKey[$index['key']] = $index;
+ }
+ }
+ $this->assertArrayHasKey($metadataIndexKey, $indexByKey, 'Metadata index should exist on destination');
+ $this->assertEquals(Database::INDEX_OBJECT, $indexByKey[$metadataIndexKey]['type']);
+ $this->assertArrayHasKey($vectorEmbeddingIndexKey, $indexByKey, 'Embeddings HNSW index should exist on destination');
+ $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $indexByKey[$vectorEmbeddingIndexKey]['type']);
+
+ // Validate VectorsDB Document
+ $response = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/documents/' . $vectorDocumentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($vectorDocumentId, $response['body']['$id']);
+ $this->assertEquals('Product Vector', $response['body']['metadata']['name']);
+
+ // ====== Cleanup all destinations ======
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $sqlDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $docsDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $vectorDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ]);
+
+ // ====== Cleanup sources ======
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $sqlDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $docsDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $vectorDatabaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $sourceProject['$id'],
+ 'x-appwrite-key' => $sourceProject['apiKey'],
+ ]);
+ }
}
diff --git a/tests/e2e/Services/Project/VariablesBase.php b/tests/e2e/Services/Project/VariablesBase.php
new file mode 100644
index 0000000000..b1f8ed61b9
--- /dev/null
+++ b/tests/e2e/Services/Project/VariablesBase.php
@@ -0,0 +1,1102 @@
+createVariable(
+ ID::unique(),
+ 'APP_KEY',
+ 'my-secret-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $this->assertNotEmpty($variable['body']['$id']);
+ $this->assertSame('APP_KEY', $variable['body']['key']);
+ $this->assertSame(true, $variable['body']['secret']);
+ $this->assertSame('', $variable['body']['value']);
+ $this->assertSame('project', $variable['body']['resourceType']);
+ $this->assertSame('', $variable['body']['resourceId']);
+
+ $dateValidator = new DatetimeValidator();
+ $this->assertSame(true, $dateValidator->isValid($variable['body']['$createdAt']));
+ $this->assertSame(true, $dateValidator->isValid($variable['body']['$updatedAt']));
+
+ // Verify via GET
+ $get = $this->getVariable($variable['body']['$id']);
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame($variable['body']['$id'], $get['body']['$id']);
+ $this->assertSame('APP_KEY', $get['body']['key']);
+
+ // Verify via LIST
+ $list = $this->listVariables(null, true);
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($list['body']['variables']));
+
+ // Cleanup
+ $this->deleteVariable($variable['body']['$id']);
+ }
+
+ public function testCreateVariableNonSecret(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'PUBLIC_KEY',
+ 'public-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $this->assertNotEmpty($variable['body']['$id']);
+ $this->assertSame('PUBLIC_KEY', $variable['body']['key']);
+ $this->assertSame(false, $variable['body']['secret']);
+ $this->assertIsBool($variable['body']['secret']);
+ $this->assertSame('public-value', $variable['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variable['body']['$id']);
+ }
+
+ public function testCreateVariableSecretValueHidden(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'SECRET_KEY',
+ 'hidden-value',
+ true
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $this->assertSame(true, $variable['body']['secret']);
+ $this->assertSame('', $variable['body']['value']);
+
+ // Verify value is also hidden on GET
+ $get = $this->getVariable($variable['body']['$id']);
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame('', $get['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variable['body']['$id']);
+ }
+
+ public function testCreateVariableWithoutAuthentication(): void
+ {
+ $response = $this->createVariable(
+ ID::unique(),
+ 'NO_AUTH_KEY',
+ 'no-auth-value',
+ null,
+ false
+ );
+
+ $this->assertSame(401, $response['headers']['status-code']);
+ }
+
+ public function testCreateVariableInvalidId(): void
+ {
+ $variable = $this->createVariable(
+ '!invalid-id!',
+ 'INVALID_ID_KEY',
+ 'value',
+ );
+
+ $this->assertSame(400, $variable['headers']['status-code']);
+ }
+
+ public function testCreateVariableMissingKey(): void
+ {
+ $response = $this->createVariable(
+ ID::unique(),
+ null,
+ 'some-value',
+ );
+
+ $this->assertSame(400, $response['headers']['status-code']);
+ }
+
+ public function testCreateVariableMissingValue(): void
+ {
+ $response = $this->createVariable(
+ ID::unique(),
+ 'MISSING_VALUE_KEY',
+ null,
+ );
+
+ $this->assertSame(400, $response['headers']['status-code']);
+ }
+
+ public function testCreateVariableDuplicateId(): void
+ {
+ $variableId = ID::unique();
+
+ $variable = $this->createVariable(
+ $variableId,
+ 'DUP_KEY_1',
+ 'value1',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+
+ // Attempt to create with same ID
+ $duplicate = $this->createVariable(
+ $variableId,
+ 'DUP_KEY_2',
+ 'value2',
+ );
+
+ $this->assertSame(409, $duplicate['headers']['status-code']);
+ $this->assertSame('variable_already_exists', $duplicate['body']['type']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testCreateVariableCustomId(): void
+ {
+ $customId = 'my-custom-variable-id';
+
+ $variable = $this->createVariable(
+ $customId,
+ 'CUSTOM_ID_KEY',
+ 'custom-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $this->assertSame($customId, $variable['body']['$id']);
+
+ // Verify via GET
+ $get = $this->getVariable($customId);
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame($customId, $get['body']['$id']);
+
+ // Cleanup
+ $this->deleteVariable($customId);
+ }
+
+ // Update variable tests
+
+ public function testUpdateVariable(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'ORIGINAL_KEY',
+ 'original-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update key and value
+ $updated = $this->updateVariable($variableId, 'UPDATED_KEY', 'updated-value');
+
+ $this->assertSame(200, $updated['headers']['status-code']);
+ $this->assertSame($variableId, $updated['body']['$id']);
+ $this->assertSame('UPDATED_KEY', $updated['body']['key']);
+ $this->assertSame('updated-value', $updated['body']['value']);
+
+ // Verify update persisted via GET
+ $get = $this->getVariable($variableId);
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame('UPDATED_KEY', $get['body']['key']);
+ $this->assertSame('updated-value', $get['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableKey(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'KEY_BEFORE',
+ 'unchanged-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only key
+ $updated = $this->updateVariable($variableId, 'KEY_AFTER');
+
+ $this->assertSame(200, $updated['headers']['status-code']);
+ $this->assertSame('KEY_AFTER', $updated['body']['key']);
+ $this->assertSame('unchanged-value', $updated['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableValue(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'UNCHANGED_KEY',
+ 'value-before',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only value
+ $updated = $this->updateVariable($variableId, null, 'value-after');
+
+ $this->assertSame(200, $updated['headers']['status-code']);
+ $this->assertSame('UNCHANGED_KEY', $updated['body']['key']);
+ $this->assertSame('value-after', $updated['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableSetSecret(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'MAKE_SECRET_KEY',
+ 'some-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $this->assertSame(false, $variable['body']['secret']);
+ $variableId = $variable['body']['$id'];
+
+ // Update to secret
+ $updated = $this->updateVariable($variableId, null, null, true);
+
+ $this->assertSame(200, $updated['headers']['status-code']);
+ $this->assertSame(true, $updated['body']['secret']);
+ $this->assertSame('', $updated['body']['value']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableCannotUnsetSecret(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'UNSET_SECRET_KEY',
+ 'secret-value',
+ true
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Attempt to unset secret
+ $updated = $this->updateVariable($variableId, null, null, false);
+
+ $this->assertSame(400, $updated['headers']['status-code']);
+ $this->assertSame('variable_cannot_unset_secret', $updated['body']['type']);
+
+ // Verify variable is unchanged
+ $get = $this->getVariable($variableId);
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame(true, $get['body']['secret']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableNoOp(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'NOOP_KEY',
+ 'noop-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update with no parameters should fail with 400
+ $updated = $this->updateVariable($variableId);
+
+ $this->assertSame(400, $updated['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableWithoutAuthentication(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'AUTH_UPDATE_KEY',
+ 'auth-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Attempt update without authentication
+ $response = $this->updateVariable($variableId, 'UPDATED_KEY', null, null, false);
+
+ $this->assertSame(401, $response['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testUpdateVariableNotFound(): void
+ {
+ $updated = $this->updateVariable('non-existent-id', 'NEW_KEY', 'new-value');
+
+ $this->assertSame(404, $updated['headers']['status-code']);
+ $this->assertSame('variable_not_found', $updated['body']['type']);
+ }
+
+ // Get variable tests
+
+ public function testGetVariable(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'GET_TEST_KEY',
+ 'get-test-value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ $get = $this->getVariable($variableId);
+
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertSame($variableId, $get['body']['$id']);
+ $this->assertSame('GET_TEST_KEY', $get['body']['key']);
+ $this->assertSame('get-test-value', $get['body']['value']);
+ $this->assertSame(false, $get['body']['secret']);
+ $this->assertSame('project', $get['body']['resourceType']);
+ $this->assertSame('', $get['body']['resourceId']);
+
+ $dateValidator = new DatetimeValidator();
+ $this->assertSame(true, $dateValidator->isValid($get['body']['$createdAt']));
+ $this->assertSame(true, $dateValidator->isValid($get['body']['$updatedAt']));
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testGetVariableNotFound(): void
+ {
+ $get = $this->getVariable('non-existent-id');
+
+ $this->assertSame(404, $get['headers']['status-code']);
+ $this->assertSame('variable_not_found', $get['body']['type']);
+ }
+
+ public function testGetVariableWithoutAuthentication(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'AUTH_GET_KEY',
+ 'auth-get-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Attempt GET without authentication
+ $response = $this->getVariable($variableId, false);
+
+ $this->assertSame(401, $response['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ // List variables tests
+
+ public function testListVariables(): void
+ {
+ // Create multiple variables
+ $variable1 = $this->createVariable(
+ ID::unique(),
+ 'LIST_KEY_ALPHA',
+ 'alpha-value',
+ false
+ );
+ $this->assertSame(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable(
+ ID::unique(),
+ 'LIST_KEY_BETA',
+ 'beta-value',
+ true
+ );
+ $this->assertSame(201, $variable2['headers']['status-code']);
+
+ $variable3 = $this->createVariable(
+ ID::unique(),
+ 'LIST_KEY_GAMMA',
+ 'gamma-value',
+ false
+ );
+ $this->assertSame(201, $variable3['headers']['status-code']);
+
+ // List all
+ $list = $this->listVariables(null, true);
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(3, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(3, \count($list['body']['variables']));
+ $this->assertIsArray($list['body']['variables']);
+
+ // Verify structure of returned variables
+ foreach ($list['body']['variables'] as $variable) {
+ $this->assertArrayHasKey('$id', $variable);
+ $this->assertArrayHasKey('$createdAt', $variable);
+ $this->assertArrayHasKey('$updatedAt', $variable);
+ $this->assertArrayHasKey('key', $variable);
+ $this->assertArrayHasKey('value', $variable);
+ $this->assertArrayHasKey('secret', $variable);
+ $this->assertArrayHasKey('resourceType', $variable);
+ $this->assertArrayHasKey('resourceId', $variable);
+ }
+
+ // Cleanup
+ $this->deleteVariable($variable1['body']['$id']);
+ $this->deleteVariable($variable2['body']['$id']);
+ $this->deleteVariable($variable3['body']['$id']);
+ }
+
+ public function testListVariablesWithLimit(): void
+ {
+ $variable1 = $this->createVariable(
+ ID::unique(),
+ 'LIMIT_KEY_1',
+ 'limit-value-1',
+ );
+ $this->assertSame(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable(
+ ID::unique(),
+ 'LIMIT_KEY_2',
+ 'limit-value-2',
+ );
+ $this->assertSame(201, $variable2['headers']['status-code']);
+
+ // List with limit of 1
+ $list = $this->listVariables([
+ Query::limit(1)->toString(),
+ ], true);
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertCount(1, $list['body']['variables']);
+ $this->assertGreaterThanOrEqual(2, $list['body']['total']);
+
+ // Cleanup
+ $this->deleteVariable($variable1['body']['$id']);
+ $this->deleteVariable($variable2['body']['$id']);
+ }
+
+ public function testListVariablesWithOffset(): void
+ {
+ $variable1 = $this->createVariable(
+ ID::unique(),
+ 'OFFSET_KEY_1',
+ 'offset-value-1',
+ );
+ $this->assertSame(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable(
+ ID::unique(),
+ 'OFFSET_KEY_2',
+ 'offset-value-2',
+ );
+ $this->assertSame(201, $variable2['headers']['status-code']);
+
+ // List all to get total
+ $listAll = $this->listVariables(null, true);
+ $this->assertSame(200, $listAll['headers']['status-code']);
+ $totalAll = \count($listAll['body']['variables']);
+
+ // List with offset
+ $listOffset = $this->listVariables([
+ Query::offset(1)->toString(),
+ ], true);
+
+ $this->assertSame(200, $listOffset['headers']['status-code']);
+ $this->assertCount($totalAll - 1, $listOffset['body']['variables']);
+
+ // Cleanup
+ $this->deleteVariable($variable1['body']['$id']);
+ $this->deleteVariable($variable2['body']['$id']);
+ }
+
+ public function testListVariablesWithoutTotal(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'NO_TOTAL_KEY',
+ 'no-total-value',
+ );
+ $this->assertSame(201, $variable['headers']['status-code']);
+
+ // List with total=false
+ $list = $this->listVariables(null, false);
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertSame(0, $list['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($list['body']['variables']));
+
+ // Cleanup
+ $this->deleteVariable($variable['body']['$id']);
+ }
+
+ public function testListVariablesCursorPagination(): void
+ {
+ $variable1 = $this->createVariable(
+ ID::unique(),
+ 'CURSOR_KEY_1',
+ 'cursor-value-1',
+ );
+ $this->assertSame(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable(
+ ID::unique(),
+ 'CURSOR_KEY_2',
+ 'cursor-value-2',
+ );
+ $this->assertSame(201, $variable2['headers']['status-code']);
+
+ // Get first page with limit 1
+ $page1 = $this->listVariables([
+ Query::limit(1)->toString(),
+ ], true);
+
+ $this->assertSame(200, $page1['headers']['status-code']);
+ $this->assertCount(1, $page1['body']['variables']);
+ $cursorId = $page1['body']['variables'][0]['$id'];
+
+ // Get next page using cursor
+ $page2 = $this->listVariables([
+ Query::limit(1)->toString(),
+ Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
+ ], true);
+
+ $this->assertSame(200, $page2['headers']['status-code']);
+ $this->assertCount(1, $page2['body']['variables']);
+ $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']);
+
+ // Cleanup
+ $this->deleteVariable($variable1['body']['$id']);
+ $this->deleteVariable($variable2['body']['$id']);
+ }
+
+ public function testListVariablesWithoutAuthentication(): void
+ {
+ $response = $this->listVariables(null, null, false);
+
+ $this->assertSame(401, $response['headers']['status-code']);
+ }
+
+ public function testListVariablesInvalidCursor(): void
+ {
+ $list = $this->listVariables([
+ Query::cursorAfter(new Document(['$id' => 'non-existent-id']))->toString(),
+ ], true);
+
+ $this->assertSame(400, $list['headers']['status-code']);
+ }
+
+ // Delete variable tests
+
+ public function testDeleteVariable(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'DELETE_KEY',
+ 'delete-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Verify it exists
+ $get = $this->getVariable($variableId);
+ $this->assertSame(200, $get['headers']['status-code']);
+
+ // Delete
+ $delete = $this->deleteVariable($variableId);
+ $this->assertSame(204, $delete['headers']['status-code']);
+ $this->assertEmpty($delete['body']);
+
+ // Verify it no longer exists
+ $get = $this->getVariable($variableId);
+ $this->assertSame(404, $get['headers']['status-code']);
+ $this->assertSame('variable_not_found', $get['body']['type']);
+ }
+
+ public function testDeleteVariableNotFound(): void
+ {
+ $delete = $this->deleteVariable('non-existent-id');
+
+ $this->assertSame(404, $delete['headers']['status-code']);
+ $this->assertSame('variable_not_found', $delete['body']['type']);
+ }
+
+ public function testDeleteVariableWithoutAuthentication(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'DELETE_AUTH_KEY',
+ 'delete-auth-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Attempt DELETE without authentication
+ $response = $this->deleteVariable($variableId, false);
+
+ $this->assertSame(401, $response['headers']['status-code']);
+
+ // Verify it still exists
+ $get = $this->getVariable($variableId);
+ $this->assertSame(200, $get['headers']['status-code']);
+
+ // Cleanup
+ $this->deleteVariable($variableId);
+ }
+
+ public function testDeleteVariableRemovedFromList(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'DELETE_LIST_KEY',
+ 'delete-list-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Get list count before delete
+ $listBefore = $this->listVariables(null, true);
+ $this->assertSame(200, $listBefore['headers']['status-code']);
+ $countBefore = $listBefore['body']['total'];
+
+ // Delete
+ $delete = $this->deleteVariable($variableId);
+ $this->assertSame(204, $delete['headers']['status-code']);
+
+ // Get list count after delete
+ $listAfter = $this->listVariables(null, true);
+ $this->assertSame(200, $listAfter['headers']['status-code']);
+ $this->assertSame($countBefore - 1, $listAfter['body']['total']);
+
+ // Verify the deleted variable is not in the list
+ $ids = \array_column($listAfter['body']['variables'], '$id');
+ $this->assertNotContains($variableId, $ids);
+ }
+
+ public function testDeleteVariableDoubleDelete(): void
+ {
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'DOUBLE_DELETE_KEY',
+ 'double-delete-value',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // First delete succeeds
+ $delete = $this->deleteVariable($variableId);
+ $this->assertSame(204, $delete['headers']['status-code']);
+
+ // Second delete returns 404
+ $delete = $this->deleteVariable($variableId);
+ $this->assertSame(404, $delete['headers']['status-code']);
+ $this->assertSame('variable_not_found', $delete['body']['type']);
+ }
+
+ // Integration tests
+
+ /**
+ * Test that project variables are available in function build and runtime.
+ */
+ public function testProjectVariableInFunction(): void
+ {
+ $projectId = $this->getProject()['$id'];
+ $apiKey = $this->getProject()['apiKey'];
+
+ // 1. Create a project variable
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'GLOBAL_VARIABLE',
+ 'Project Variable Value',
+ false
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // 2. Create a function with build commands that echo the variable
+ $function = $this->client->call(Client::METHOD_POST, '/functions', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ], [
+ 'functionId' => ID::unique(),
+ 'name' => 'Project Variable Test',
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'execute' => ['any'],
+ 'timeout' => 15,
+ 'commands' => 'echo $GLOBAL_VARIABLE',
+ ]);
+
+ $this->assertSame(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ // 3. Deploy the function (basic function reads GLOBAL_VARIABLE from env)
+ $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', [
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ], [
+ 'code' => $this->packageCode('functions', 'basic'),
+ 'activate' => true,
+ ]);
+
+ $this->assertSame(202, $deployment['headers']['status-code']);
+ $deploymentId = $deployment['body']['$id'] ?? '';
+
+ // 4. Wait for deployment to be ready and activated
+ $this->assertEventually(function () use ($projectId, $apiKey, $functionId, $deploymentId) {
+ $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+
+ $status = $deployment['body']['status'] ?? '';
+ if ($status === 'failed') {
+ throw new Critical('Deployment build failed: ' . ($deployment['body']['buildLogs'] ?? 'no logs'));
+ }
+
+ $this->assertSame('ready', $status, 'Deployment status is not ready');
+ }, 120000, 500);
+
+ $this->assertEventually(function () use ($projectId, $apiKey, $functionId, $deploymentId) {
+ $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->assertSame($deploymentId, $function['body']['deploymentId'] ?? '');
+ }, 120000, 500);
+
+ // 5. Verify the project variable was available during build
+ $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->assertSame(200, $deployment['headers']['status-code']);
+ $this->assertStringContainsString('Project Variable Value', $deployment['body']['buildLogs']);
+
+ // 6. Execute the function and verify the project variable is in runtime output
+ $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), [
+ 'async' => false,
+ ]);
+
+ $this->assertSame(201, $execution['headers']['status-code']);
+ $this->assertSame('completed', $execution['body']['status']);
+ $this->assertSame(200, $execution['body']['responseStatusCode']);
+ $output = json_decode($execution['body']['responseBody'], true);
+ $this->assertSame('Project Variable Value', $output['GLOBAL_VARIABLE']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->deleteVariable($variableId);
+ }
+
+ /**
+ * Test that project variables are available in site build and SSR runtime.
+ */
+ public function testProjectVariableInSite(): void
+ {
+ $projectId = $this->getProject()['$id'];
+ $apiKey = $this->getProject()['apiKey'];
+
+ // 1. Create a project variable
+ $variable = $this->createVariable(
+ ID::unique(),
+ 'name',
+ 'ProjectVarTest',
+ );
+
+ $this->assertSame(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // 2. Create a site
+ $site = $this->client->call(Client::METHOD_POST, '/sites', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ], [
+ 'siteId' => ID::unique(),
+ 'name' => 'Project Variable Astro Site',
+ 'framework' => 'astro',
+ 'adapter' => 'ssr',
+ 'buildRuntime' => 'node-22',
+ 'outputDirectory' => './dist',
+ 'buildCommand' => 'echo $name && npm run build',
+ 'installCommand' => 'npm ci',
+ 'fallbackFile' => '',
+ ]);
+
+ $this->assertSame(201, $site['headers']['status-code']);
+ $siteId = $site['body']['$id'];
+
+ // 3. Setup domain for proxy access
+ $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
+ $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), [
+ 'domain' => ID::unique() . '.' . $sitesDomain,
+ 'siteId' => $siteId,
+ ]);
+
+ $this->assertSame(201, $rule['headers']['status-code']);
+
+ // 4. Deploy the site (astro site reads import.meta.env.name)
+ $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', [
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ], [
+ 'code' => $this->packageCode('sites', 'astro'),
+ 'activate' => 'true',
+ ]);
+
+ $this->assertSame(202, $deployment['headers']['status-code']);
+ $deploymentId = $deployment['body']['$id'] ?? '';
+
+ // 5. Wait for deployment to be ready and activated
+ $this->assertEventually(function () use ($projectId, $apiKey, $siteId, $deploymentId) {
+ $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+
+ $status = $deployment['body']['status'] ?? '';
+ if ($status === 'failed') {
+ throw new Critical('Site deployment failed: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
+ }
+
+ $this->assertSame('ready', $status, 'Deployment status is not ready');
+ }, 120000, 500);
+
+ $this->assertEventually(function () use ($projectId, $apiKey, $siteId, $deploymentId) {
+ $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->assertSame($deploymentId, $site['body']['deploymentId'] ?? '');
+ }, 120000, 500);
+
+ // 6. Verify the project variable was available during build
+ $deployment = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->assertSame(200, $deployment['headers']['status-code']);
+ $this->assertStringContainsString('ProjectVarTest', $deployment['body']['buildLogs']);
+
+ // 7. Get the domain and access the site
+ $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::equal('deploymentResourceId', [$siteId])->toString(),
+ Query::equal('trigger', ['manual'])->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ ],
+ ]);
+
+ $this->assertSame(200, $rules['headers']['status-code']);
+ $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules']));
+ $domain = $rules['body']['rules'][0]['domain'];
+
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://' . $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/');
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertStringContainsString('Env variable is ProjectVarTest', $response['body']);
+ $this->assertStringNotContainsString('Variable not found', $response['body']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ]);
+ $this->deleteVariable($variableId);
+ }
+
+ // Helpers
+
+ protected function createVariable(string $variableId, ?string $key, ?string $value, ?bool $secret = null, bool $authenticated = true): mixed
+ {
+ $params = [
+ 'variableId' => $variableId,
+ ];
+
+ if ($key !== null) {
+ $params['key'] = $key;
+ }
+
+ if ($value !== null) {
+ $params['value'] = $value;
+ }
+
+ if ($secret !== null) {
+ $params['secret'] = $secret;
+ }
+
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+
+ if ($authenticated) {
+ $headers = array_merge($headers, $this->getHeaders());
+ }
+
+ return $this->client->call(Client::METHOD_POST, '/project/variables', $headers, $params);
+ }
+
+ protected function updateVariable(string $variableId, ?string $key = null, ?string $value = null, ?bool $secret = null, bool $authenticated = true): mixed
+ {
+ $params = [];
+
+ if ($key !== null) {
+ $params['key'] = $key;
+ }
+
+ if ($value !== null) {
+ $params['value'] = $value;
+ }
+
+ if ($secret !== null) {
+ $params['secret'] = $secret;
+ }
+
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+
+ if ($authenticated) {
+ $headers = array_merge($headers, $this->getHeaders());
+ }
+
+ return $this->client->call(Client::METHOD_PUT, '/project/variables/' . $variableId, $headers, $params);
+ }
+
+ protected function getVariable(string $variableId, bool $authenticated = true): mixed
+ {
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+
+ if ($authenticated) {
+ $headers = array_merge($headers, $this->getHeaders());
+ }
+
+ return $this->client->call(Client::METHOD_GET, '/project/variables/' . $variableId, $headers);
+ }
+
+ /**
+ * @param array|null $queries
+ */
+ protected function listVariables(?array $queries, ?bool $total, bool $authenticated = true): mixed
+ {
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+
+ if ($authenticated) {
+ $headers = array_merge($headers, $this->getHeaders());
+ }
+
+ return $this->client->call(Client::METHOD_GET, '/project/variables', $headers, [
+ 'queries' => $queries,
+ 'total' => $total,
+ ]);
+ }
+
+ protected function deleteVariable(string $variableId, bool $authenticated = true): mixed
+ {
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+
+ if ($authenticated) {
+ $headers = array_merge($headers, $this->getHeaders());
+ }
+
+ return $this->client->call(Client::METHOD_DELETE, '/project/variables/' . $variableId, $headers);
+ }
+
+ protected function packageCode(string $type, string $name): CURLFile
+ {
+ $folderPath = realpath(__DIR__ . '/../../../resources/' . $type) . "/$name";
+ $tarPath = "$folderPath/code.tar.gz";
+
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
+
+ if (filesize($tarPath) > 1024 * 1024 * 5) {
+ throw new \Exception('Code package is too large. Use the chunked upload method instead.');
+ }
+
+ return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
+ }
+}
diff --git a/tests/e2e/Services/Project/VariablesConsoleClientTest.php b/tests/e2e/Services/Project/VariablesConsoleClientTest.php
new file mode 100644
index 0000000000..b969dd49e7
--- /dev/null
+++ b/tests/e2e/Services/Project/VariablesConsoleClientTest.php
@@ -0,0 +1,14 @@
+client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
@@ -734,7 +734,7 @@ class WebhooksCustomServerTest extends Scope
$stdout = '';
$folder = 'timeout';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz";
- Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
+ Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php
index 231ec302de..ced3a0e23d 100644
--- a/tests/e2e/Services/Projects/ProjectsBase.php
+++ b/tests/e2e/Services/Projects/ProjectsBase.php
@@ -274,6 +274,7 @@ trait ProjectsBase
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -288,6 +289,7 @@ trait ProjectsBase
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
index d4945f8407..3f84529943 100644
--- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
+++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
@@ -13,6 +13,8 @@ use Tests\E2E\Scopes\SideClient;
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;
use Utopia\System\System;
@@ -106,6 +108,111 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(401, $response['headers']['status-code']);
}
+ public function testDeleteProjectWithMultiDB(): void
+ {
+ // Create a team and project
+ $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'teamId' => ID::unique(),
+ 'name' => 'MultiDB Team',
+ ]);
+
+ $this->assertEquals(201, $team['headers']['status-code']);
+ $teamId = $team['body']['$id'];
+
+ $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'projectId' => ID::unique(),
+ 'name' => 'MultiDB Project',
+ 'teamId' => $teamId,
+ 'region' => System::getEnv('_APP_REGION', 'default')
+ ]);
+
+ $this->assertEquals(201, $project['headers']['status-code']);
+ $projectId = $project['body']['$id'];
+
+ $projectAdminHeaders = array_merge($this->getHeaders(), [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-mode' => 'admin',
+ ]);
+
+ // Create legacy database and collection
+ $database = $this->client->call(Client::METHOD_POST, '/databases', $projectAdminHeaders, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Legacy DB',
+ ]);
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $projectAdminHeaders, [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Legacy Collection',
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $collection['headers']['status-code']);
+
+ // Create documentsdb database and collection
+ $documentsDb = $this->client->call(Client::METHOD_POST, '/documentsdb', $projectAdminHeaders, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Documents DB',
+ ]);
+ $this->assertEquals(201, $documentsDb['headers']['status-code']);
+ $documentsDbId = $documentsDb['body']['$id'];
+
+ $documentsCollection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $documentsDbId . '/collections', $projectAdminHeaders, [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Documents Collection',
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $documentsCollection['headers']['status-code']);
+
+ // Create vectorsdb database and collection
+ $vectorDb = $this->client->call(Client::METHOD_POST, '/vectorsdb', $projectAdminHeaders, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Vector DB',
+ ]);
+ $this->assertEquals(201, $vectorDb['headers']['status-code']);
+ $vectorDbId = $vectorDb['body']['$id'];
+
+ $vectorCollection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $vectorDbId . '/collections', $projectAdminHeaders, [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Vector Collection',
+ 'dimension' => 3,
+ 'documentSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $vectorCollection['headers']['status-code']);
+
+ // Delete project
+ $delete = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(204, $delete['headers']['status-code']);
+
+ // Ensure project is gone
+ $getProject = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(404, $getProject['headers']['status-code']);
+ }
+
public function testCreateDuplicateProject(): void
{
// Create a team
@@ -4579,6 +4686,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -4595,6 +4703,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -4613,6 +4722,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_CREATE',
'value' => 'ANOTHERTESTINGVALUE'
]);
@@ -4625,6 +4735,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => str_repeat("A", 256),
'value' => 'TESTINGVALUE'
]);
@@ -4637,6 +4748,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $data['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'LONGKEY',
'value' => str_repeat("#", 8193),
]);
@@ -4782,18 +4894,6 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains("APP_TEST_UPDATE", $variableKeys);
$this->assertContains("APP_TEST_UPDATE_1", $variableKeys);
- /**
- * Test for FAILURE
- */
-
- $response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $data['projectId'],
- 'x-appwrite-mode' => 'admin',
- ], $this->getHeaders()));
-
- $this->assertEquals(400, $response['headers']['status-code']);
-
$response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
@@ -4802,6 +4902,19 @@ class ProjectsConsoleClientTest extends Scope
'value' => 'TESTINGVALUEUPDATED_2'
]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertSame('TESTINGVALUEUPDATED_2', $response['body']['value']);
+ $this->assertSame('APP_TEST_UPDATE', $response['body']['key']);
+
+ /**
+ * Test for FAILURE
+ */
+ $response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['variableId'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $data['projectId'],
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()));
+
$this->assertEquals(400, $response['headers']['status-code']);
$longKey = str_repeat("A", 256);
@@ -4851,6 +4964,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_DELETE',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -4865,6 +4979,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $projectData['projectId'],
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
+ 'variableId' => 'unique()',
'key' => 'APP_TEST_DELETE_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php
index 81b11d1041..59a853bfc8 100644
--- a/tests/e2e/Services/Proxy/ProxyBase.php
+++ b/tests/e2e/Services/Proxy/ProxyBase.php
@@ -271,7 +271,7 @@ trait ProxyBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
@@ -288,7 +288,7 @@ trait ProxyBase
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
index d1d7d0d054..f6200ed209 100644
--- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
+++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
@@ -3884,4 +3884,1368 @@ class RealtimeCustomClientTest extends Scope
}
});
}
+ public function testChannelTablesDB()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['documents', 'collections'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_' . $projectId . '=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test Database Create
+ */
+ $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' => 'Actors DB',
+ ]);
+
+ $databaseId = $database['body']['$id'];
+
+ /**
+ * Test Collection Create
+ */
+ $actors = $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' => 'Actors',
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ $actorsId = $actors['body']['$id'];
+
+ $name = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/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, $name['headers']['status-code']);
+ $this->assertEquals('name', $name['body']['key']);
+ $this->assertEquals('string', $name['body']['type']);
+ $this->assertEquals(256, $name['body']['size']);
+ $this->assertTrue($name['body']['required']);
+
+ sleep(2);
+
+ /**
+ * Test Document Create
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'rowId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Chris Evans'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $rowId = $document['body']['$id'];
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(8, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $rowId, $response['data']['channels']);
+ $this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+ $this->assertContains('databases.' . $databaseId . '.tables.' . $actorsId . '.rows.' . $rowId, $response['data']['channels']);
+ $this->assertContains('databases.' . $databaseId . '.tables.' . $actorsId . '.rows', $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('Chris Evans', $response['data']['payload']['name']);
+
+ /**
+ * Test Document Update
+ */
+ $document = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'rowId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Chris Evans 2'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.tables.{$actorsId}.rows", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals('Chris Evans 2', $response['data']['payload']['name']);
+
+ /**
+ * Test Document Delete
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'rowId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Bradley Cooper'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $client->receive();
+
+ $rowId = $document['body']['$id'];
+
+ $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('rows', $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $response['data']['channels']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.tables.{$actorsId}.rows", $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.{$rowId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.{$rowId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('Bradley Cooper', $response['data']['payload']['name']);
+
+ // test bulk create
+ $documents = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$actorsId}/rows", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'rows' => [
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Robert Downey Jr.',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ],
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Scarlett Johansson',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ ],
+ ]);
+
+ // Receive first document event
+
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
+
+ // Receive second document event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.create", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
+
+ // test bulk update
+ $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $actorsId . '/rows/', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'data' => [
+ 'name' => 'Marvel Hero',
+ '$permissions' => [
+ Permission::read(Role::user($this->getUser()['$id'])),
+ Permission::update(Role::user($this->getUser()['$id'])),
+ Permission::delete(Role::user($this->getUser()['$id'])),
+ ]
+ ],
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Receive first document update event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Receive second document update event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Receive third document update event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.update", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Test bulk delete
+ $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$actorsId}/rows", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Receive first document delete event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.delete", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // Receive second document delete event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // Receive third document delete event
+ $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->assertCount(8, $response['data']['channels']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // bulk upsert
+ $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$actorsId}/rows", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'rows' => [
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Robert Downey Jr.',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ ],
+ ]);
+
+ $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->assertCount(8, $response['data']['channels']);
+
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.*.collections.*", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("databases.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("databases.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("databases.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*.upsert", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.upsert", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*.upsert", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}.rows.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*", $response['data']['events']);
+ $this->assertContains("tablesdb.*.tables.{$actorsId}", $response['data']['events']);
+ $this->assertContains("tablesdb.{$databaseId}.tables.*.rows.*.upsert", $response['data']['events']);
+ $this->assertContains("tablesdb.*", $response['data']['events']);
+
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ $client->close();
+ }
+ public function testChannelDocumentsdb()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['documents', 'collections'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_' . $projectId . '=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test Database Create
+ */
+ $database = $this->client->call(Client::METHOD_POST, '/documentsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Actors DB',
+ ]);
+
+ $databaseId = $database['body']['$id'];
+
+ /**
+ * Test Collection Create
+ */
+ $actors = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Actors',
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ 'documentSecurity' => true,
+ ]);
+
+ $actorsId = $actors['body']['$id'];
+
+ /**
+ * Test Document Create
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Chris Evans'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $documentId = $document['body']['$id'];
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
+ $this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+ $this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
+ $this->assertContains('documentsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('Chris Evans', $response['data']['payload']['name']);
+
+ /**
+ * Test Document Update
+ */
+ $document = $this->client->call(Client::METHOD_PATCH, '/documentsdb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Chris Evans 2'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals('Chris Evans 2', $response['data']['payload']['name']);
+
+ /**
+ * Test Document Delete
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Bradley Cooper'
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $client->receive();
+
+ $documentId = $document['body']['$id'];
+
+ $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('Bradley Cooper', $response['data']['payload']['name']);
+
+ // test bulk create
+ $documents = $this->client->call(Client::METHOD_POST, "/documentsdb/{$databaseId}/collections/{$actorsId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Robert Downey Jr.',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ],
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Scarlett Johansson',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ ],
+ ]);
+
+ // Receive first document event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
+
+ // Receive second document event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::read(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::update(Role::any()), $response['data']['payload']['$permissions']);
+ $this->assertContains(Permission::delete(Role::any()), $response['data']['payload']['$permissions']);
+
+ // test bulk update
+ $response = $this->client->call(Client::METHOD_PATCH, '/documentsdb/' . $databaseId . '/collections/' . $actorsId . '/documents/', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'data' => [
+ 'name' => 'Marvel Hero',
+ '$permissions' => [
+ Permission::read(Role::user($this->getUser()['$id'])),
+ Permission::update(Role::user($this->getUser()['$id'])),
+ Permission::delete(Role::user($this->getUser()['$id'])),
+ ]
+ ],
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Receive first document update event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Receive second document update event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Receive third document update event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertEquals('Marvel Hero', $response['data']['payload']['name']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+
+ // Test bulk delete
+ $response = $this->client->call(Client::METHOD_DELETE, "/documentsdb/{$databaseId}/collections/{$actorsId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Receive first document delete event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // Receive second document delete event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // Receive third document delete event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ // bulk upsert
+ $this->client->call(Client::METHOD_PUT, "/documentsdb/{$databaseId}/collections/{$actorsId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ '$id' => ID::unique(),
+ 'name' => 'Robert Downey Jr.',
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ ],
+ ]);
+
+ $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->assertCount(3, $response['data']['channels']);
+
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}.documents.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*", $response['data']['events']);
+ $this->assertContains("documentsdb.*.collections.{$actorsId}", $response['data']['events']);
+ $this->assertContains("documentsdb.{$databaseId}.collections.*.documents.*.upsert", $response['data']['events']);
+ $this->assertContains("documentsdb.*", $response['data']['events']);
+
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+ $this->assertArrayHasKey('$id', $response['data']['payload']);
+ $this->assertArrayHasKey('name', $response['data']['payload']);
+ $this->assertArrayHasKey('$permissions', $response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['$permissions']);
+
+ $client->close();
+ }
+
+ public function testChannelVectorsDB()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['documents', 'collections'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_' . $projectId . '=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ // Create VectorsDB database
+ $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Actors VDB',
+ ]);
+
+ $databaseId = $database['body']['$id'];
+
+ // Create collection in VectorsDB
+ $actors = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Actors',
+ 'permissions' => [
+ Permission::create(Role::user($this->getUser()['$id'])),
+ ],
+ 'documentSecurity' => true,
+ 'dimension' => 3,
+ ]);
+
+ $actorsId = $actors['body']['$id'];
+
+ // Create document in VectorsDB
+ $document = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['name' => 'Chris Evans']
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $documentId = $document['body']['$id'];
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ // vectorsdb channels should include 3 items like documentsdb
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['embeddings']);
+ $this->assertCount(3, $response['data']['payload']['embeddings']);
+ $this->assertEquals('Chris Evans', $response['data']['payload']['metadata']['name']);
+
+ // Update document
+ $this->client->call(Client::METHOD_PATCH, '/vectorsdb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'data' => [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['name' => 'Chris Evans 2']
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']['embeddings']);
+ $this->assertEquals('Chris Evans 2', $response['data']['payload']['metadata']['name']);
+
+ // Delete document
+ $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
+
+ // Bulk create two documents
+ $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$actorsId}/documents", array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'documents' => [
+ [
+ 'embeddings' => [1.0, 0.0, 0.0],
+ 'metadata' => ['name' => 'Robert Downey Jr.'],
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ],
+ [
+ 'embeddings' => [0.0, 1.0, 0.0],
+ 'metadata' => ['name' => 'Scarlett Johansson'],
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]
+ ],
+ ]);
+
+ // Receive first bulk document event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $response['data']['payload']['$id'] . '.create', $response['data']['events']);
+ $this->assertContains('vectorsdb.*.collections.*.documents.*.create', $response['data']['events']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.*.documents.*.create', $response['data']['events']);
+ $this->assertContains('vectorsdb.*.collections.' . $actorsId . '.documents.*.create', $response['data']['events']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertIsArray($response['data']['payload']);
+
+ // Receive second bulk document event
+ $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->assertCount(3, $response['data']['channels']);
+ $this->assertContains('vectorsdb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $response['data']['payload']['$id'] . '.create', $response['data']['events']);
+
+ $client->close();
+ }
}
diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php
index b940dda742..c3377faad8 100644
--- a/tests/e2e/Services/Sites/SitesBase.php
+++ b/tests/e2e/Services/Sites/SitesBase.php
@@ -241,7 +241,7 @@ trait SitesBase
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
- Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
diff --git a/tests/resources/csv/vectorsdb-documents.csv b/tests/resources/csv/vectorsdb-documents.csv
new file mode 100644
index 0000000000..b0b970703e
--- /dev/null
+++ b/tests/resources/csv/vectorsdb-documents.csv
@@ -0,0 +1,3 @@
+$id,embeddings,metadata
+vector-doc-1,"[0.15,0.25,0.35]","{""title"":""Vector Alpha"",""category"":""science""}"
+vector-doc-2,"[0.55,0.65,0.75]","{""title"":""Vector Beta"",""category"":""history""}"
\ No newline at end of file
diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml
index 02593f8123..47a69f077b 100644
--- a/tests/resources/docker/docker-compose.yml
+++ b/tests/resources/docker/docker-compose.yml
@@ -76,6 +76,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_STORAGE_ANTIVIRUS=disabled
- _APP_STORAGE_LIMIT
@@ -141,6 +142,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER
appwrite-worker-tasks:
entrypoint: worker-tasks
@@ -162,6 +164,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DB_ADAPTER
appwrite-worker-deletes:
entrypoint: worker-deletes
@@ -182,6 +185,7 @@ services:
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_DB_HOST
+ - _APP_DB_ADAPTER
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
diff --git a/tests/resources/postgresql/Dockerfile b/tests/resources/postgresql/Dockerfile
deleted file mode 100644
index a731833b48..0000000000
--- a/tests/resources/postgresql/Dockerfile
+++ /dev/null
@@ -1,11 +0,0 @@
-ARG POSTGRES_VERSION=17
-FROM postgres:${POSTGRES_VERSION}
-
-ARG POSTGRES_VERSION=17
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- postgresql-${POSTGRES_VERSION}-postgis-3 \
- postgresql-${POSTGRES_VERSION}-postgis-3-scripts \
- postgresql-${POSTGRES_VERSION}-pgvector \
- && rm -rf /var/lib/apt/lists/*
diff --git a/tests/unit/Event/MockPublisher.php b/tests/unit/Event/MockPublisher.php
index 0b812e7032..a7118d3c09 100644
--- a/tests/unit/Event/MockPublisher.php
+++ b/tests/unit/Event/MockPublisher.php
@@ -23,7 +23,7 @@ class MockPublisher implements Publisher
return $this->events[$queue] ?? null;
}
- public function retry(Queue $queue, int $limit = null): void
+ public function retry(Queue $queue, ?int $limit = null): void
{
// TODO: Implement retry() method.
}
diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php
index 598a47a901..fc2d839ca6 100644
--- a/tests/unit/Messaging/MessagingChannelsTest.php
+++ b/tests/unit/Messaging/MessagingChannelsTest.php
@@ -16,7 +16,7 @@ class MessagingChannelsTest extends TestCase
*/
public $connectionsPerChannel = 10;
- public Realtime $realtime;
+ public ?Realtime $realtime = null;
public $connectionsCount = 0;
public $connectionsAuthenticated = 0;
public $connectionsGuest = 0;
@@ -125,7 +125,7 @@ class MessagingChannelsTest extends TestCase
public function tearDown(): void
{
- unset($this->realtime);
+ $this->realtime = null;
$this->connectionsCount = 0;
}
diff --git a/tests/unit/Platform/Modules/Installer/ModuleTest.php b/tests/unit/Platform/Modules/Installer/ModuleTest.php
index 8df452d8de..0b7e7effcb 100644
--- a/tests/unit/Platform/Modules/Installer/ModuleTest.php
+++ b/tests/unit/Platform/Modules/Installer/ModuleTest.php
@@ -5,6 +5,7 @@ namespace Tests\Unit\Platform\Modules\Installer;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Error;
use Appwrite\Platform\Installer\Http\Installer\Install;
+use Appwrite\Platform\Installer\Http\Installer\Reset;
use Appwrite\Platform\Installer\Http\Installer\Shutdown;
use Appwrite\Platform\Installer\Http\Installer\Status;
use Appwrite\Platform\Installer\Http\Installer\Validate;
@@ -41,13 +42,15 @@ class ModuleTest extends TestCase
$service = reset($services);
$actions = $service->getActions();
- $this->assertCount(6, $actions);
+ $this->assertCount(8, $actions);
$this->assertArrayHasKey('installerView', $actions);
$this->assertArrayHasKey('installerStatus', $actions);
$this->assertArrayHasKey('installerValidate', $actions);
$this->assertArrayHasKey('installerComplete', $actions);
$this->assertArrayHasKey('installerShutdown', $actions);
+ $this->assertArrayHasKey('installerReset', $actions);
$this->assertArrayHasKey('installerInstall', $actions);
+ $this->assertArrayHasKey('installerCertificateGet', $actions);
}
public function testViewAction(): void
@@ -108,6 +111,18 @@ class ModuleTest extends TestCase
$this->assertActionInjects($action, ['request', 'response', 'swooleServer']);
}
+ public function testResetAction(): void
+ {
+ $action = $this->getAction('installerReset');
+
+ $this->assertEquals('installerReset', Reset::getName());
+ $this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
+ $this->assertEquals('/install/reset', $action->getHttpPath());
+ $this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
+ $this->assertActionParams($action, ['installId', 'hard']);
+ $this->assertActionInjects($action, ['request', 'response', 'installerState', 'installerConfig']);
+ }
+
public function testInstallAction(): void
{
$action = $this->getAction('installerInstall');
@@ -206,6 +221,7 @@ class ModuleTest extends TestCase
$this->assertEquals('installerValidate', Validate::getName());
$this->assertEquals('installerComplete', Complete::getName());
$this->assertEquals('installerShutdown', Shutdown::getName());
+ $this->assertEquals('installerReset', Reset::getName());
$this->assertEquals('installerInstall', Install::getName());
$this->assertEquals('installerError', Error::getName());
}
@@ -221,6 +237,7 @@ class ModuleTest extends TestCase
$this->assertInstanceOf(Validate::class, $actions['installerValidate']);
$this->assertInstanceOf(Complete::class, $actions['installerComplete']);
$this->assertInstanceOf(Shutdown::class, $actions['installerShutdown']);
+ $this->assertInstanceOf(Reset::class, $actions['installerReset']);
$this->assertInstanceOf(Install::class, $actions['installerInstall']);
}
@@ -239,7 +256,7 @@ class ModuleTest extends TestCase
public function testPostRoutesUsePostMethod(): void
{
- $postActions = ['installerValidate', 'installerComplete', 'installerShutdown', 'installerInstall'];
+ $postActions = ['installerValidate', 'installerComplete', 'installerShutdown', 'installerReset', 'installerInstall'];
foreach ($postActions as $name) {
$action = $this->getAction($name);
$this->assertEquals(