mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into copilot/add-copilot-setup-steps-file
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -394,7 +394,8 @@ jobs:
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations
|
||||
Migrations,
|
||||
Project
|
||||
]
|
||||
include:
|
||||
- service: Databases
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' => [
|
||||
[
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
||||
return [
|
||||
'collections' => [
|
||||
'$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' => [],
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -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 => [
|
||||
|
||||
@@ -62,6 +62,8 @@ $admins = [
|
||||
'devKeys.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'health.read',
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
+60
-235
@@ -1,25 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DateTimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Http::get('/v1/project/usage')
|
||||
@@ -62,16 +52,33 @@ Http::get('/v1/project/usage')
|
||||
METRIC_EXECUTIONS_MB_SECONDS,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
METRIC_DOCUMENTS,
|
||||
METRIC_DOCUMENTS_DOCUMENTSDB,
|
||||
METRIC_DATABASES,
|
||||
METRIC_DATABASES_DOCUMENTSDB,
|
||||
METRIC_USERS,
|
||||
METRIC_BUCKETS,
|
||||
METRIC_FILES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
|
||||
METRIC_DEPLOYMENTS_STORAGE,
|
||||
METRIC_BUILDS_STORAGE,
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
|
||||
METRIC_FILES_IMAGES_TRANSFORMED,
|
||||
// VectorsDB totals
|
||||
METRIC_DATABASES_VECTORSDB,
|
||||
METRIC_COLLECTIONS_VECTORSDB,
|
||||
METRIC_DOCUMENTS_VECTORSDB,
|
||||
METRIC_DATABASES_STORAGE_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
|
||||
// Embeddings totals
|
||||
METRIC_EMBEDDINGS_TEXT,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
|
||||
],
|
||||
'period' => [
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', []))
|
||||
|
||||
+19
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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());
|
||||
|
||||
+31
-3
@@ -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',
|
||||
|
||||
+183
-12
@@ -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']);
|
||||
|
||||
@@ -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: <?php echo $organization; ?>/console:7.6.4
|
||||
image: <?php echo $organization; ?>/console:7.8.26
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -240,6 +240,9 @@
|
||||
if (key === 'database') {
|
||||
value = toDatabaseLabel(formState?.database);
|
||||
}
|
||||
if (key === 'emailCertificates' && !value) {
|
||||
value = formState?.accountEmail;
|
||||
}
|
||||
if (value) {
|
||||
node.textContent = value;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<span class="install-counter typography-text-xs-400" data-install-counter></span>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="install-global-actions is-hidden" data-install-global-actions>
|
||||
<button type="button" class="button secondary" data-install-start-over>
|
||||
<span class="button-text typography-text-m-500">Start Over</span>
|
||||
</button>
|
||||
<button type="button" class="button secondary" data-install-hard-reset>
|
||||
<span class="button-text typography-text-m-500">Reset Everything</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
+5
-18
@@ -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,
|
||||
|
||||
Generated
+24
-57
@@ -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"
|
||||
}
|
||||
|
||||
+56
-14
@@ -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:
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Database.
|
||||
@@ -0,0 +1 @@
|
||||
Decrement a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a document by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
|
||||
@@ -0,0 +1 @@
|
||||
Delete an index.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
|
||||
@@ -0,0 +1 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
|
||||
@@ -0,0 +1 @@
|
||||
Get index by ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get the database activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Increment a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
List attributes in the collection.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
List indexes in the collection.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Update a collection by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Update a database by its unique ID.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Executable → Regular
+1
-1
@@ -15,4 +15,4 @@ adminDb.createUser({
|
||||
roles: [
|
||||
{ role: 'readWrite', db: database }
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Installer\Http\Installer\Certificate;
|
||||
|
||||
use Appwrite\Platform\Installer\Validator\AppDomain;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
private const int CONNECTION_TIMEOUT_SECONDS = 5;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'installerCertificateGet';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->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 '';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Installer\Http\Installer;
|
||||
|
||||
use Appwrite\Platform\Installer\Runtime\Config;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Appwrite\Platform\Installer\Server;
|
||||
use Utopia\Http\Adapter\Swoole\Request;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Reset extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'installerReset';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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').
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+38
@@ -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.
|
||||
*
|
||||
|
||||
+6
-4
@@ -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)
|
||||
|
||||
+6
-4
@@ -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)
|
||||
|
||||
+7
-5
@@ -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(
|
||||
|
||||
+7
-5
@@ -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,
|
||||
|
||||
+8
-6
@@ -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,
|
||||
|
||||
+11
-9
@@ -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(
|
||||
|
||||
+7
-4
@@ -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
|
||||
);
|
||||
|
||||
+12
-7
@@ -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);
|
||||
|
||||
|
||||
+4
-2
@@ -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]);
|
||||
}
|
||||
|
||||
+9
-7
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user