mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/sync-mongodb' into feat-platform-db-access
This commit is contained in:
@@ -39,14 +39,23 @@ _APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mongodb
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mariadb
|
||||
_APP_DB_HOST=mariadb
|
||||
_APP_DB_PORT=3306
|
||||
_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=
|
||||
|
||||
@@ -47,6 +47,36 @@ Examples:
|
||||
'resourceType' => 'deployments'
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Document Update Optimization
|
||||
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical Security Practices
|
||||
|
||||
+7
-7
@@ -111,7 +111,7 @@ $ git push origin [name_of_your_new_branch]
|
||||
|
||||
## Setup From Source
|
||||
|
||||
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and create run the docker-compose stack.
|
||||
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and run the docker-compose stack.
|
||||
|
||||
> If you just want to install Appwrite for day-to-day use and not as a contributor, you can reference the [installation guide](https://github.com/appwrite/appwrite#installation), the [getting started guide](https://appwrite.io/docs/quick-starts), or the main [README](README.md) file.
|
||||
|
||||
@@ -173,12 +173,12 @@ Learn more at our [Technology Stack](#technology-stack) section.
|
||||
- [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - Appwrite console architecture
|
||||
|
||||
##### Container Namespace Conventions
|
||||
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on it's intended use.
|
||||
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on its intended use.
|
||||
|
||||
`appwrite-worker-X` - Workers (`src/Appwrite/Platform/Workers/*`)
|
||||
`appwrite-task-X` - Tasks (`src/Appwrite/Platform/Tasks/*`)
|
||||
|
||||
Other containes should be named the same as their service, for example `redis` should just be called `redis`.
|
||||
Other containers should be named the same as their service, for example `redis` should just be called `redis`.
|
||||
|
||||
##### Security
|
||||
|
||||
@@ -189,7 +189,7 @@ Other containes should be named the same as their service, for example `redis` s
|
||||
|
||||
## Modules
|
||||
|
||||
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesnt contain all product's features such as workers or tasks. While there might still be some occurances of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
|
||||
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesn't contain all product's features such as workers or tasks. While there might still be some occurrences of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
|
||||
|
||||
### HTTP Endpoints
|
||||
|
||||
@@ -204,7 +204,7 @@ Tips and tricks:
|
||||
1. If endpoint doesn't have resource, use service name as resource name too
|
||||
> Example: `Modules/Sites/Http/Sites/Get.php`
|
||||
|
||||
2. If there are multiple resources, use then all in folder structure
|
||||
2. If there are multiple resources, use them all in folder structure
|
||||
> Example: `Modules/Sites/Http/Deployments/Builds/Create.php`
|
||||
|
||||
3. Action can only be `Get`, `Create`, `Update`, `Delete` or `XList`
|
||||
@@ -395,7 +395,7 @@ These are the current metrics we collect usage stats for:
|
||||
|
||||
> Note: The curly brackets in the metric name represents a template and is replaced with a value when the metric is processed.
|
||||
|
||||
Metrics are collected within 3 scopes Daily, monthly, an infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
|
||||
Metrics are collected within 3 scopes Daily, monthly, and infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
|
||||
statistics ,via API or via background worker. For both cases you will need to add a `const` variable in `app/init.php` under the usage metrics list using the naming convention `METRIC_<RESOURCE_NAME>` as shown below.
|
||||
|
||||
```php
|
||||
@@ -661,7 +661,7 @@ docker compose exec redis redis-cli FLUSHALL
|
||||
|
||||
## Using preview domains locally
|
||||
|
||||
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
|
||||
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud workspaces such as Gitpod or GitHub Codespaces.
|
||||
|
||||
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/sdks && \
|
||||
chmod +x /usr/local/bin/specs && \
|
||||
chmod +x /usr/local/bin/ssl && \
|
||||
chmod +x /usr/local/bin/time-travel && \
|
||||
chmod +x /usr/local/bin/screenshot && \
|
||||
chmod +x /usr/local/bin/test && \
|
||||
chmod +x /usr/local/bin/upgrade && \
|
||||
|
||||
@@ -318,6 +318,10 @@ $setResource('logError', function (Registry $register) {
|
||||
|
||||
$setResource('executor', fn () => new Executor(), []);
|
||||
|
||||
$setResource('bus', function (Registry $register) use ($cli) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$cli
|
||||
|
||||
@@ -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' => true,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -777,6 +786,7 @@ return [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// At the moment, always empty (no runtime supports it yet)
|
||||
'array' => false,
|
||||
'$id' => ID::custom('startCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -787,17 +797,6 @@ return [
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('specification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildSpecification'),
|
||||
@@ -1255,17 +1254,6 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('specification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildSpecification'),
|
||||
@@ -2190,13 +2178,6 @@ return [
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_function_internal_id'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resourceInternalId'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_resourceType'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
|
||||
@@ -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' => [],
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -27,7 +27,7 @@ return [
|
||||
Exception::GENERAL_RESOURCE_BLOCKED => [
|
||||
'name' => Exception::GENERAL_RESOURCE_BLOCKED,
|
||||
'description' => 'Access to this resource is blocked.',
|
||||
'code' => 401,
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::GENERAL_UNKNOWN_ORIGIN => [
|
||||
'name' => Exception::GENERAL_UNKNOWN_ORIGIN,
|
||||
@@ -168,8 +168,8 @@ return [
|
||||
],
|
||||
Exception::USER_BLOCKED => [
|
||||
'name' => Exception::USER_BLOCKED,
|
||||
'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.',
|
||||
'code' => 401,
|
||||
'description' => 'The current user has been blocked.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::USER_INVALID_TOKEN => [
|
||||
'name' => Exception::USER_INVALID_TOKEN,
|
||||
@@ -1201,6 +1201,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 => [
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ return [
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
|
||||
@@ -288,7 +288,10 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'emailVerification' => $user->getAttribute('emailVerification'),
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
} catch (\Throwable $th) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
|
||||
}
|
||||
@@ -1032,7 +1035,11 @@ Http::post('/v1/account/sessions/email')
|
||||
->setAttribute('password', $proofForPasswordUpdated->hash($password))
|
||||
->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName())
|
||||
->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions());
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'hash' => $user->getAttribute('hash'),
|
||||
'hashOptions' => $user->getAttribute('hashOptions'),
|
||||
]));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
@@ -1822,7 +1829,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
|
||||
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
|
||||
$dbForProject->updateDocument('identities', $identity->getId(), new Document([
|
||||
'providerAccessToken' => $identity->getAttribute('providerAccessToken'),
|
||||
'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'),
|
||||
'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'),
|
||||
]));
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('email'))) {
|
||||
@@ -1960,7 +1971,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setAttribute('sessionId', $session->getId())
|
||||
->setAttribute('sessionInternalId', $session->getSequence());
|
||||
|
||||
$dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'sessionId' => $target->getAttribute('sessionId'),
|
||||
'sessionInternalId' => $target->getAttribute('sessionInternalId'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3145,7 +3159,9 @@ Http::patch('/v1/account/name')
|
||||
|
||||
$user->setAttribute('name', $name);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $user->getAttribute('name'),
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -3798,13 +3814,15 @@ Http::put('/v1/account/recovery')
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
->setAttribute('passwordUpdate', DateTime::now())
|
||||
->setAttribute('hash', $proofForPassword->getHash()->getName())
|
||||
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
|
||||
->setAttribute('emailVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(
|
||||
[
|
||||
'password' => $newPassword,
|
||||
'passwordHistory' => $history,
|
||||
'passwordUpdate' => DateTime::now(),
|
||||
'hash' => $proofForPassword->getHash()->getName(),
|
||||
'hashOptions' => $proofForPassword->getHash()->getOptions(),
|
||||
'emailVerification' => true]
|
||||
));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4126,7 +4144,7 @@ Http::put('/v1/account/verifications/email')
|
||||
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true]));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4342,7 +4360,7 @@ Http::put('/v1/account/verifications/phone')
|
||||
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true]));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4500,7 +4518,11 @@ Http::put('/v1/account/targets/:targetId/push')
|
||||
|
||||
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
|
||||
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'identifier' => $target->getAttribute('identifier'),
|
||||
'expired' => $target->getAttribute('expired'),
|
||||
'name' => $target->getAttribute('name'),
|
||||
]));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
|
||||
@@ -46,6 +46,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')
|
||||
@@ -430,8 +440,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,
|
||||
@@ -560,13 +578,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' => '{}',
|
||||
@@ -769,26 +797,17 @@ 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) {
|
||||
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
->inject('getDatabasesDB')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response, callable $getDatabasesDB) {
|
||||
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key, $getDatabasesDB);
|
||||
$report = $appwrite->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -827,21 +846,14 @@ Http::get('/v1/migrations/firebase/report')
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
|
||||
}
|
||||
|
||||
$firebase = new Firebase($serviceAccount);
|
||||
|
||||
try {
|
||||
$firebase = new Firebase($serviceAccount);
|
||||
$report = $firebase->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -876,21 +888,14 @@ Http::get('/v1/migrations/supabase/report')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
|
||||
try {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
$report = $supabase->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -925,21 +930,14 @@ Http::get('/v1/migrations/nhost/report')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
|
||||
try {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
$report = $nhost->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
|
||||
@@ -62,16 +62,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 +97,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 +389,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 +402,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,6 +432,14 @@ 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -1161,7 +1161,7 @@ Http::patch('/v1/users/:userId/status')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1204,7 +1204,7 @@ Http::put('/v1/users/:userId/labels')
|
||||
|
||||
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1245,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1289,7 +1289,7 @@ Http::patch('/v1/users/:userId/name')
|
||||
|
||||
$user->setAttribute('name', $name);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -1344,7 +1344,10 @@ Http::patch('/v1/users/:userId/password')
|
||||
->setAttribute('password', '')
|
||||
->setAttribute('passwordUpdate', DateTime::now());
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
|
||||
]));
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
}
|
||||
@@ -1377,7 +1380,13 @@ Http::patch('/v1/users/:userId/password')
|
||||
->setAttribute('hash', $hasher->getName())
|
||||
->setAttribute('hashOptions', $hasher->getOptions());
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'passwordHistory' => $user->getAttribute('passwordHistory'),
|
||||
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
|
||||
'hash' => $user->getAttribute('hash'),
|
||||
'hashOptions' => $user->getAttribute('hashOptions'),
|
||||
]));
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
|
||||
@@ -1469,7 +1478,15 @@ Http::patch('/v1/users/:userId/email')
|
||||
;
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'email' => $user->getAttribute('email'),
|
||||
'emailVerification' => $user->getAttribute('emailVerification'),
|
||||
'emailCanonical' => $user->getAttribute('emailCanonical'),
|
||||
'emailIsCanonical' => $user->getAttribute('emailIsCanonical'),
|
||||
'emailIsCorporate' => $user->getAttribute('emailIsCorporate'),
|
||||
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
|
||||
'emailIsFree' => $user->getAttribute('emailIsFree'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
@@ -1477,7 +1494,8 @@ Http::patch('/v1/users/:userId/email')
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
if (\strlen($email) !== 0) {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email]));
|
||||
$oldTarget->setAttribute('identifier', $email);
|
||||
} else {
|
||||
$dbForProject->deleteDocument('targets', $oldTarget->getId());
|
||||
}
|
||||
@@ -1542,12 +1560,15 @@ Http::patch('/v1/users/:userId/phone')
|
||||
|
||||
$oldPhone = $user->getAttribute('phone');
|
||||
|
||||
// Store null instead of empty string so unique constraint allows multiple users without phone
|
||||
$phoneValue = $number !== '' ? $number : null;
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $number)
|
||||
->setAttribute('phone', $phoneValue)
|
||||
->setAttribute('phoneVerification', false)
|
||||
;
|
||||
|
||||
if (\strlen($number) !== 0) {
|
||||
if ($number !== '') {
|
||||
$target = $dbForProject->findOne('targets', [
|
||||
Query::equal('identifier', [$number]),
|
||||
]);
|
||||
@@ -1558,20 +1579,24 @@ Http::patch('/v1/users/:userId/phone')
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'phone' => $phoneValue,
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
if (\strlen($number) !== 0) {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
|
||||
if ($number !== '') {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
|
||||
$oldTarget->setAttribute('identifier', $number);
|
||||
} else {
|
||||
$dbForProject->deleteDocument('targets', $oldTarget->getId());
|
||||
}
|
||||
} else {
|
||||
if (\strlen($number) !== 0) {
|
||||
if ($number !== '') {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
@@ -1630,7 +1655,7 @@ Http::patch('/v1/users/:userId/verification')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -1668,7 +1693,7 @@ Http::patch('/v1/users/:userId/prefs')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1768,7 +1793,13 @@ Http::patch('/v1/users/:userId/targets/:targetId')
|
||||
$target->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'identifier' => $target->getAttribute('identifier'),
|
||||
'expired' => $target->getAttribute('expired'),
|
||||
'providerId' => $target->getAttribute('providerId'),
|
||||
'providerInternalId' => $target->getAttribute('providerInternalId'),
|
||||
'name' => $target->getAttribute('name'),
|
||||
]));
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
@@ -1836,7 +1867,7 @@ Http::patch('/v1/users/:userId/mfa')
|
||||
|
||||
$user->setAttribute('mfa', $mfa);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -2024,7 +2055,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes')
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -2096,7 +2127,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes')
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
+69
-119
@@ -5,11 +5,11 @@ require_once __DIR__ . '/../init.php';
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -30,11 +30,14 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
|
||||
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Table;
|
||||
use Utopia\Bus\Bus;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -62,7 +65,7 @@ Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
if (!empty($previewHostname)) {
|
||||
@@ -131,8 +134,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +324,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
};
|
||||
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$runtime = match ($type) {
|
||||
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
@@ -550,6 +554,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($deployment->getAttribute('startCommand', ''))) {
|
||||
$startCommand = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
|
||||
}
|
||||
|
||||
$runtimeEntrypoint = match ($version) {
|
||||
'v2' => '',
|
||||
default => "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$startCommand\"",
|
||||
@@ -706,10 +714,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
} finally {
|
||||
if ($type === 'function' || $type === 'site') {
|
||||
$queueForExecutions
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,70 +764,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
->setStatusCode($execution['responseStatusCode'] ?? 200)
|
||||
->send($body);
|
||||
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
|
||||
foreach ($apiKey->getDisabledMetrics() as $key) {
|
||||
$queueForStatsUsage->disableMetric($key);
|
||||
}
|
||||
}
|
||||
|
||||
$metricTypeExecutions = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS);
|
||||
$metricTypeIdExecutions = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS);
|
||||
$metricTypeExecutionsCompute = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE);
|
||||
$metricTypeIdExecutionsCompute = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE);
|
||||
$metricTypeExecutionsMbSeconds = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS);
|
||||
$metricTypeIdExecutionsMBSeconds = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS);
|
||||
if ($deployment->getAttribute('resourceType') === 'sites') {
|
||||
$queueForStatsUsage
|
||||
->disableMetric(METRIC_NETWORK_REQUESTS)
|
||||
->disableMetric(METRIC_NETWORK_INBOUND)
|
||||
->disableMetric(METRIC_NETWORK_OUTBOUND);
|
||||
if ($resource->getAttribute('adapter') !== 'ssr') {
|
||||
$queueForStatsUsage
|
||||
->disableMetric(METRIC_EXECUTIONS)
|
||||
->disableMetric(METRIC_EXECUTIONS_COMPUTE)
|
||||
->disableMetric(METRIC_EXECUTIONS_MB_SECONDS)
|
||||
->disableMetric($metricTypeExecutions)
|
||||
->disableMetric($metricTypeIdExecutions)
|
||||
->disableMetric($metricTypeExecutionsCompute)
|
||||
->disableMetric($metricTypeIdExecutionsCompute)
|
||||
->disableMetric($metricTypeExecutionsMbSeconds)
|
||||
->disableMetric($metricTypeIdExecutionsMBSeconds);
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES_REQUESTS, 1)
|
||||
->addMetric(METRIC_SITES_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_SITES_OUTBOUND, $response->getSize())
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_REQUESTS), 1)
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_INBOUND), $request->getSize() + $fileSize)
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_OUTBOUND), $response->getSize())
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
$compute = (int)($execution->getAttribute('duration') * 1000);
|
||||
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric($metricTypeExecutions, 1)
|
||||
->addMetric($metricTypeIdExecutions, 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute) // per project
|
||||
->addMetric($metricTypeExecutionsCompute, $compute) // per function
|
||||
->addMetric($metricTypeIdExecutionsCompute, $compute) // per function
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
|
||||
->addMetric($metricTypeExecutionsMbSeconds, $mbSeconds)
|
||||
->addMetric($metricTypeIdExecutionsMBSeconds, $mbSeconds)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
deployment: $deployment->getArrayCopy(),
|
||||
));
|
||||
|
||||
/* cleanup */
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
@@ -881,9 +833,8 @@ Http::init()
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('geodb')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('platform')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -894,7 +845,7 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -902,7 +853,7 @@ Http::init()
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -988,17 +939,23 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.4.0', '<')) {
|
||||
$response->addFilter(new ResponseV16());
|
||||
if (version_compare($responseFormat, '1.9.0', '<')) {
|
||||
$response->addFilter(new ResponseV21());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.5.0', '<')) {
|
||||
$response->addFilter(new ResponseV17());
|
||||
if (version_compare($responseFormat, '1.8.0', '<')) {
|
||||
$response->addFilter(new ResponseV20());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.7.0', '<')) {
|
||||
$response->addFilter(new ResponseV19());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.6.0', '<')) {
|
||||
$response->addFilter(new ResponseV18());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.7.0', '<')) {
|
||||
$response->addFilter(new ResponseV19());
|
||||
if (version_compare($responseFormat, '1.5.0', '<')) {
|
||||
$response->addFilter(new ResponseV17());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.4.0', '<')) {
|
||||
$response->addFilter(new ResponseV16());
|
||||
}
|
||||
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
|
||||
$warnings[] = "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks";
|
||||
@@ -1178,8 +1135,7 @@ Http::options()
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1192,14 +1148,14 @@ Http::options()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1215,12 +1171,11 @@ Http::options()
|
||||
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
|
||||
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
|
||||
*/
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
});
|
||||
|
||||
Http::error()
|
||||
@@ -1231,10 +1186,10 @@ Http::error()
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('bus')
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1307,21 +1262,12 @@ Http::error()
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
if ($logger && $publish) {
|
||||
@@ -1453,6 +1399,9 @@ Http::error()
|
||||
$sdk = $route?->getLabel("sdk", false);
|
||||
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
|
||||
if (!empty($sdk)) {
|
||||
if (\is_array($sdk)) {
|
||||
$sdk = $sdk[0];
|
||||
}
|
||||
/** @var \Appwrite\SDK\Method $sdk */
|
||||
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
|
||||
} elseif ($route === null) {
|
||||
@@ -1568,8 +1517,7 @@ Http::get('/robots.txt')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1579,13 +1527,13 @@ Http::get('/robots.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1603,8 +1551,7 @@ Http::get('/humans.txt')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1614,13 +1561,13 @@ Http::get('/humans.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1718,7 +1665,10 @@ Http::get('/v1/ping')
|
||||
->setAttribute('pingedAt', $pingedAt);
|
||||
|
||||
$authorization->skip(function () use ($dbForPlatform, $project) {
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'pingCount' => $project->getAttribute('pingCount'),
|
||||
'pingedAt' => $project->getAttribute('pingedAt')
|
||||
]));
|
||||
});
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
@@ -21,6 +22,7 @@ use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Bus\Bus;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
@@ -212,6 +214,8 @@ Http::init()
|
||||
}
|
||||
|
||||
if (!$dbKey) {
|
||||
\var_dump($apiKey);
|
||||
\var_dump($request->getHeader('x-appwrite-key', ''));
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -375,8 +379,9 @@ Http::init()
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,9 +391,13 @@ Http::init()
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
]));
|
||||
} else {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,6 +487,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, StatsUsage $queueForStatsUsage, 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', []))
|
||||
@@ -664,7 +679,9 @@ Http::init()
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
|
||||
'transformedAt' => $file->getAttribute('transformedAt')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,7 +780,8 @@ Http::shutdown()
|
||||
->inject('authorization')
|
||||
->inject('timelimit')
|
||||
->inject('eventProcessor')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
|
||||
->inject('bus')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -962,7 +980,9 @@ Http::shutdown()
|
||||
}
|
||||
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$cacheLog->setAttribute('accessedAt', $now);
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
|
||||
'accessedAt' => $cacheLog->getAttribute('accessedAt')
|
||||
])));
|
||||
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
|
||||
$cache->save($key, $data['payload']);
|
||||
}
|
||||
@@ -975,16 +995,11 @@ Http::shutdown()
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
|
||||
+26
-1
@@ -188,10 +188,16 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
Http::setResource('bus', function ($register, $utopia) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
|
||||
}, ['register', 'utopia']);
|
||||
|
||||
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;
|
||||
@@ -405,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);
|
||||
|
||||
@@ -577,6 +599,9 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
|
||||
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
|
||||
if (!empty($sdk)) {
|
||||
if (\is_array($sdk)) {
|
||||
$sdk = $sdk[0];
|
||||
}
|
||||
/** @var Appwrite\SDK\Method $sdk */
|
||||
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
|
||||
} elseif ($route === null) {
|
||||
|
||||
@@ -94,6 +94,7 @@ const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=
|
||||
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
|
||||
const APP_COMPUTE_MEMORY_DEFAULT = 512;
|
||||
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
||||
const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
@@ -290,6 +291,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';
|
||||
@@ -364,6 +404,12 @@ const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
|
||||
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
|
||||
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
|
||||
|
||||
// Realtime metrics
|
||||
const METRIC_REALTIME_CONNECTIONS = 'realtime.connections';
|
||||
const METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT = 'realtime.messages.sent';
|
||||
const METRIC_REALTIME_INBOUND = 'realtime.inbound';
|
||||
const METRIC_REALTIME_OUTBOUND = 'realtime.outbound';
|
||||
|
||||
// Resource types
|
||||
const RESOURCE_TYPE_PROJECTS = 'projects';
|
||||
const RESOURCE_TYPE_FUNCTIONS = 'functions';
|
||||
@@ -376,6 +422,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';
|
||||
@@ -397,3 +444,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;
|
||||
@@ -137,6 +140,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;
|
||||
@@ -145,9 +150,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
|
||||
@@ -212,9 +220,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());
|
||||
@@ -238,6 +249,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());
|
||||
|
||||
+40
-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',
|
||||
@@ -420,6 +448,7 @@ $register->set('smtp', function () {
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
@@ -449,3 +478,11 @@ $register->set('promiseAdapter', function () {
|
||||
$register->set('hooks', function () {
|
||||
return new Hooks();
|
||||
});
|
||||
$listeners = require __DIR__ . '/../listeners.php';
|
||||
$register->set('bus', function () use ($listeners) {
|
||||
$bus = new \Utopia\Bus\Bus();
|
||||
foreach ($listeners as $listener) {
|
||||
$bus->subscribe($listener);
|
||||
}
|
||||
return $bus;
|
||||
});
|
||||
|
||||
+196
-19
@@ -10,7 +10,6 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
@@ -32,6 +31,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;
|
||||
@@ -160,9 +161,6 @@ Http::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
Http::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
@@ -554,7 +552,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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, Authorization $authorization, Request $request) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -662,7 +660,31 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$dbForProject->getCache()->purge($cacheKey);
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
|
||||
/**
|
||||
* 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, StatsUsage $queueForStatsUsage) use ($getDatabaseTypePrefixedMetric, $databaseType) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
@@ -694,7 +716,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
$metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES);
|
||||
$queueForStatsUsage->addMetric($metric, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
@@ -703,9 +726,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);
|
||||
$queueForStatsUsage
|
||||
->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) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
@@ -715,10 +740,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);
|
||||
$queueForStatsUsage
|
||||
->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
|
||||
$queueForStatsUsage
|
||||
@@ -776,6 +804,9 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$queueForWebhooks = new Webhook($publisherWebhooks);
|
||||
$queueForRealtime = new Realtime();
|
||||
|
||||
// to get consumed in the stats worker without calling to database
|
||||
$queueForStatsUsage->setContext('database', new Document(['type' => $databaseType]));
|
||||
|
||||
|
||||
$database
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
@@ -799,7 +830,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization', 'request']);
|
||||
|
||||
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
|
||||
@@ -820,6 +851,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, StatsUsage $queueForStatsUsage, Authorization $authorization) {
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $queueForStatsUsage, $authorization): Database {
|
||||
$databaseDSN = $database->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 ($queueForStatsUsage, $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;
|
||||
$queueForStatsUsage
|
||||
->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 ($queueForStatsUsage, $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;
|
||||
$queueForStatsUsage
|
||||
->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 ($queueForStatsUsage, $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;
|
||||
$queueForStatsUsage
|
||||
->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 ($queueForStatsUsage, $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;
|
||||
$queueForStatsUsage
|
||||
->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 ($queueForStatsUsage, $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;
|
||||
$queueForStatsUsage
|
||||
->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','queueForStatsUsage','authorization']);
|
||||
|
||||
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = [];
|
||||
|
||||
@@ -1239,7 +1402,9 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
$accessedAt = $key->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'accessedAt' => $key->getAttribute('accessedAt')
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
@@ -1256,7 +1421,10 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'sdks' => $key->getAttribute('sdks'),
|
||||
'accessedAt' => $key->getAttribute('accessedAt')
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
@@ -1413,7 +1581,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
|
||||
$accessedAt = $token->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
|
||||
$token->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([
|
||||
'accessedAt' => $token->getAttribute('accessedAt')
|
||||
])));
|
||||
}
|
||||
|
||||
return new Document([
|
||||
@@ -1430,9 +1600,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)) {
|
||||
@@ -1441,3 +1611,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']);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Bus\Listeners\Log;
|
||||
use Appwrite\Bus\Listeners\Usage;
|
||||
|
||||
return [
|
||||
new Log(),
|
||||
new Usage(),
|
||||
];
|
||||
+100
-11
@@ -224,6 +224,13 @@ if (!function_exists('getTelemetry')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -356,7 +363,10 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
->setAttribute('timestamp', DateTime::now())
|
||||
->setAttribute('value', json_encode($payload));
|
||||
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([
|
||||
'timestamp' => $statsDocument->getAttribute('timestamp'),
|
||||
'value' => $statsDocument->getAttribute('value')
|
||||
])));
|
||||
} catch (Throwable $th) {
|
||||
logError($th, "updateWorkerDocument");
|
||||
}
|
||||
@@ -545,20 +555,41 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$outboundBytes = 0;
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$data = $event['data'];
|
||||
$data['subscriptions'] = $group['subscriptions'];
|
||||
|
||||
$server->send($group['ids'], json_encode([
|
||||
$payloadJson = json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $data
|
||||
]));
|
||||
$total += count($group['ids']);
|
||||
]);
|
||||
|
||||
$server->send($group['ids'], $payloadJson);
|
||||
|
||||
$count = count($group['ids']);
|
||||
$total += $count;
|
||||
$outboundBytes += strlen($payloadJson) * $count;
|
||||
}
|
||||
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
if (!empty($projectId)) {
|
||||
$metrics = [
|
||||
METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT => $total,
|
||||
];
|
||||
|
||||
if ($outboundBytes > 0) {
|
||||
$metrics[METRIC_REALTIME_OUTBOUND] = $outboundBytes;
|
||||
}
|
||||
|
||||
triggerStats($metrics, $projectId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
@@ -635,6 +666,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
|
||||
}
|
||||
|
||||
$rawSize = $request->getSize();
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_INBOUND => $rawSize,
|
||||
], $project->getId());
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack.
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication.
|
||||
@@ -689,14 +726,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
$server->send([$connection], json_encode([
|
||||
$connectedPayloadJson = json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => $names,
|
||||
'subscriptions' => $mapping,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
@@ -707,6 +746,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
|
||||
$connectedOutboundBytes = \strlen($connectedPayloadJson);
|
||||
|
||||
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
|
||||
|
||||
|
||||
} catch (Throwable $th) {
|
||||
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
|
||||
|
||||
@@ -748,6 +793,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
|
||||
@@ -760,7 +806,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
if ($projectId !== 'console') {
|
||||
if (!empty($projectId) && $projectId !== 'console') {
|
||||
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
|
||||
|
||||
$database = getProjectDB($project);
|
||||
@@ -786,17 +832,41 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many messages.');
|
||||
}
|
||||
|
||||
// Record realtime inbound bytes for this project
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_INBOUND => $rawSize,
|
||||
], $project->getId());
|
||||
}
|
||||
|
||||
$message = json_decode($message, true);
|
||||
|
||||
if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
|
||||
}
|
||||
|
||||
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
|
||||
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
|
||||
}
|
||||
|
||||
switch ($message['type']) {
|
||||
case 'ping':
|
||||
$server->send([$connection], json_encode([
|
||||
$pongPayloadJson = json_encode([
|
||||
'type' => 'pong'
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $pongPayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$pongOutboundBytes = \strlen($pongPayloadJson);
|
||||
|
||||
if ($pongOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'authentication':
|
||||
@@ -857,14 +927,27 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
}
|
||||
|
||||
$user = $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$server->send([$connection], json_encode([
|
||||
|
||||
$authResponsePayloadJson = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'authentication',
|
||||
'success' => true,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $authResponsePayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$authOutboundBytes = \strlen($authResponsePayloadJson);
|
||||
|
||||
if ($authOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -905,6 +988,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
$register->get('telemetry.connectionCounter')->add(-1);
|
||||
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_CONNECTIONS => -1,
|
||||
], $projectId);
|
||||
}
|
||||
$realtime->unsubscribe($connection);
|
||||
|
||||
|
||||
+58
-4
@@ -9,7 +9,6 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
@@ -220,6 +219,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', '');
|
||||
$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
|
||||
});
|
||||
@@ -355,9 +408,6 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
@@ -542,6 +592,10 @@ try {
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
Server::setResource('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$worker
|
||||
->error()
|
||||
->inject('error')
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/cli.php time-travel "$@"
|
||||
+5
-4
@@ -19,7 +19,8 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Appwrite\\": "src/Appwrite",
|
||||
"Executor\\": "src/Executor"
|
||||
"Executor\\": "src/Executor",
|
||||
"Utopia\\Bus\\": "src/Utopia/Bus"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -48,7 +49,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.*",
|
||||
@@ -58,6 +58,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.*",
|
||||
@@ -69,7 +70,7 @@
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "dev-feat-platform-db-access as 1.5.0",
|
||||
"utopia-php/migration": "dev-feat-platform-db-access as 1.8.0",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
@@ -99,6 +100,7 @@
|
||||
"swoole/ide-helper": "6.*",
|
||||
"phpstan/phpstan": "1.12.*",
|
||||
"textalk/websocket": "1.5.*",
|
||||
"czproject/git-php": "4.*",
|
||||
"laravel/pint": "1.*",
|
||||
"phpbench/phpbench": "1.*"
|
||||
},
|
||||
@@ -107,7 +109,6 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
|
||||
Generated
+113
-52
@@ -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": "44e4887b0166d881da56e99dfcd284a4",
|
||||
"content-hash": "4ad48ed13cdfd66a288bb1529e731ead",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2708,16 +2708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.5",
|
||||
"version": "v7.4.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2785,7 +2785,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-27T16:16:02+00:00"
|
||||
"time": "2026-03-05T11:16:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.4",
|
||||
"version": "5.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5"
|
||||
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
|
||||
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255",
|
||||
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3902,9 +3902,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.4"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.8"
|
||||
},
|
||||
"time": "2026-02-24T00:37:36+00:00"
|
||||
"time": "2026-03-11T01:03:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4267,16 +4267,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.33.40",
|
||||
"version": "0.33.41",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1"
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
|
||||
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4310,9 +4310,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.40"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.41"
|
||||
},
|
||||
"time": "2026-02-19T13:00:08+00:00"
|
||||
"time": "2026-02-24T12:01:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4521,12 +4521,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "06ea808b3191b985ebaae105c0fe5569094e68b6"
|
||||
"reference": "4939c67c82c03bfcd7aace9d91cccc6e45fc698c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/06ea808b3191b985ebaae105c0fe5569094e68b6",
|
||||
"reference": "06ea808b3191b985ebaae105c0fe5569094e68b6",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/4939c67c82c03bfcd7aace9d91cccc6e45fc698c",
|
||||
"reference": "4939c67c82c03bfcd7aace9d91cccc6e45fc698c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4568,7 +4568,7 @@
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/feat-platform-db-access"
|
||||
},
|
||||
"time": "2026-03-03T16:06:55+00:00"
|
||||
"time": "2026-03-14T10:41:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5215,16 +5215,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920"
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5258,9 +5258,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.1"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
|
||||
},
|
||||
"time": "2026-02-27T12:18:49+00:00"
|
||||
"time": "2026-02-25T11:36:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5438,16 +5438,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.3",
|
||||
"version": "1.11.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11"
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/45d22c0107a53bb9a0a4e39db0e738d461631d11",
|
||||
"reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5483,22 +5483,22 @@
|
||||
"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.3"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
|
||||
},
|
||||
"time": "2026-02-27T06:54:59+00:00"
|
||||
"time": "2026-02-25T07:15:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
"version": "v7.19.1",
|
||||
"version": "v7.19.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paratestphp/paratest.git",
|
||||
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7"
|
||||
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/95b03194f4cdf5c83175ceead673e21cb66465e7",
|
||||
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
|
||||
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5512,7 +5512,7 @@
|
||||
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
|
||||
"phpunit/php-file-iterator": "^6.0.1 || ^7",
|
||||
"phpunit/php-timer": "^8 || ^9",
|
||||
"phpunit/phpunit": "^12.5.14 || ^13.0.5",
|
||||
"phpunit/phpunit": "^12.5.9 || ^13",
|
||||
"sebastian/environment": "^8.0.3 || ^9",
|
||||
"symfony/console": "^7.4.4 || ^8.0.4",
|
||||
"symfony/process": "^7.4.5 || ^8.0.5"
|
||||
@@ -5522,10 +5522,10 @@
|
||||
"ext-pcntl": "*",
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"phpstan/phpstan": "^2.1.40",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.4",
|
||||
"phpstan/phpstan-phpunit": "^2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.10",
|
||||
"phpstan/phpstan": "^2.1.38",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.12",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.8",
|
||||
"symfony/filesystem": "^7.4.0 || ^8.0.1"
|
||||
},
|
||||
"bin": [
|
||||
@@ -5566,7 +5566,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.1"
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5578,7 +5578,71 @@
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-25T14:53:45+00:00"
|
||||
"time": "2026-02-06T10:53:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
"version": "v4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/czproject/git-php.git",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Pecha",
|
||||
"email": "janpecha@email.cz"
|
||||
}
|
||||
],
|
||||
"description": "Library for work with Git repository in PHP.",
|
||||
"keywords": [
|
||||
"git"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/czproject/git-php/issues",
|
||||
"source": "https://github.com/czproject/git-php/tree/v4.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/janpecha",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.janpecha.cz/donate/git-php/",
|
||||
"type": "other"
|
||||
},
|
||||
{
|
||||
"url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA",
|
||||
"type": "stripe"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/czproject",
|
||||
"type": "thanks.dev"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-10T07:24:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
@@ -9045,8 +9109,8 @@
|
||||
{
|
||||
"package": "utopia-php/migration",
|
||||
"version": "dev-feat-platform-db-access",
|
||||
"alias": "1.5.0",
|
||||
"alias_normalized": "1.5.0.0"
|
||||
"alias": "1.8.0",
|
||||
"alias_normalized": "1.8.0.0"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
@@ -9073,8 +9137,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
+56
-27
@@ -104,6 +104,7 @@ services:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- coredns
|
||||
- ollama
|
||||
# - clamav
|
||||
entrypoint:
|
||||
- php
|
||||
@@ -152,7 +153,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _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
|
||||
@@ -289,6 +295,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -305,7 +312,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _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
|
||||
@@ -325,6 +337,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -340,7 +353,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
@@ -359,6 +371,7 @@ services:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- request-catcher-sms
|
||||
- request-catcher-webhook
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -371,7 +384,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -390,6 +402,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
@@ -399,6 +412,7 @@ services:
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -414,7 +428,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -456,9 +469,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
|
||||
@@ -474,7 +489,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _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
|
||||
@@ -496,6 +516,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -513,7 +534,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -629,6 +649,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
@@ -658,7 +679,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
@@ -722,7 +742,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -808,7 +827,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -851,6 +869,7 @@ services:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./tests:/usr/src/code/tests
|
||||
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
environment:
|
||||
@@ -875,7 +894,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -918,7 +936,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -994,7 +1011,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1028,7 +1044,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1051,6 +1066,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1062,7 +1078,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1085,6 +1100,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1100,7 +1116,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
@@ -1116,6 +1131,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1131,7 +1147,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
entrypoint: schedule-messages
|
||||
@@ -1146,6 +1161,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1161,7 +1177,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-assistant:
|
||||
@@ -1238,7 +1253,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
|
||||
@@ -1257,7 +1271,6 @@ services:
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
|
||||
mongodb:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
@@ -1294,7 +1307,6 @@ services:
|
||||
start_period: 30s
|
||||
|
||||
appwrite-mongo-express:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo-express
|
||||
container_name: appwrite-mongo-express
|
||||
networks:
|
||||
@@ -1309,25 +1321,41 @@ services:
|
||||
- mongodb
|
||||
|
||||
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/data: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}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
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
|
||||
<<: *x-logging
|
||||
@@ -1567,3 +1595,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
Executable → Regular
+1
-1
@@ -15,4 +15,4 @@ adminDb.createUser({
|
||||
roles: [
|
||||
{ role: 'readWrite', db: database }
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
parameters:
|
||||
level: 8
|
||||
paths:
|
||||
- src/Utopia/Bus
|
||||
- src/Appwrite/Bus
|
||||
- src/Appwrite/Transformation
|
||||
bootstrapFiles:
|
||||
- app/init/constants.php
|
||||
scanDirectories:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
stopOnError="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<extensions>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Events;
|
||||
|
||||
use Utopia\Bus\Event;
|
||||
|
||||
class ExecutionCompleted implements Event
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $execution
|
||||
* @param array<string, mixed> $project
|
||||
* @param array<string, mixed> $spec
|
||||
* @param array<string, mixed> $resource
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $execution,
|
||||
public readonly array $project,
|
||||
public readonly array $spec = [],
|
||||
public readonly array $resource = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Events;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Bus\Event;
|
||||
|
||||
class RequestCompleted implements Event
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $project
|
||||
* @param array<string, mixed> $deployment
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $project,
|
||||
public readonly Request $request,
|
||||
public readonly Response $response,
|
||||
public readonly array $deployment = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Event\Execution;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'log';
|
||||
}
|
||||
|
||||
public static function getEvents(): array
|
||||
{
|
||||
return [ExecutionCompleted::class];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Persists execution logs to database via queue')
|
||||
->inject('publisher')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(ExecutionCompleted $event, Publisher $publisher): void
|
||||
{
|
||||
$queueForExecutions = new Execution($publisher);
|
||||
$queueForExecutions
|
||||
->setExecution(new Document($event->execution))
|
||||
->setProject(new Document($event->project))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Utopia\Bus\Event;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
class Usage extends Listener
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'usage';
|
||||
}
|
||||
|
||||
public static function getEvents(): array
|
||||
{
|
||||
return [
|
||||
ExecutionCompleted::class,
|
||||
RequestCompleted::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Records usage metrics')
|
||||
->inject('publisherStatsUsage')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(Event $event, Publisher $publisher): void
|
||||
{
|
||||
match (true) {
|
||||
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisher),
|
||||
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $publisher),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisher): void
|
||||
{
|
||||
$execution = new Document($event->execution);
|
||||
$resource = new Document($event->resource);
|
||||
|
||||
// Non-SSR sites don't record execution metrics
|
||||
if ($execution->getAttribute('resourceType') === 'sites' && $resource->getAttribute('adapter') !== 'ssr') {
|
||||
return;
|
||||
}
|
||||
$project = new Document($event->project);
|
||||
$spec = $event->spec;
|
||||
|
||||
$resourceType = $execution->getAttribute('resourceType', '');
|
||||
$resourceInternalId = $execution->getAttribute('resourceInternalId', '');
|
||||
$duration = $execution->getAttribute('duration', 0);
|
||||
|
||||
$compute = (int)($duration * 1000);
|
||||
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $duration * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
|
||||
|
||||
$queueForStatsUsage = new StatsUsage($publisher);
|
||||
$queueForStatsUsage
|
||||
->setProject($project)
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), $compute)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), $compute)
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), $mbSeconds)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), $mbSeconds)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
private function handleRequestCompleted(RequestCompleted $event, Publisher $publisher): void
|
||||
{
|
||||
$fileSize = 0;
|
||||
$file = $event->request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$project = new Document($event->project);
|
||||
$deployment = new Document($event->deployment);
|
||||
$queueForStatsUsage = new StatsUsage($publisher);
|
||||
|
||||
$inbound = $event->request->getSize() + $fileSize;
|
||||
$outbound = $event->response->getSize();
|
||||
|
||||
$queueForStatsUsage->setProject($project);
|
||||
|
||||
if ($deployment->getAttribute('resourceType') === 'sites') {
|
||||
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES_REQUESTS, 1)
|
||||
->addMetric(METRIC_SITES_INBOUND, $inbound)
|
||||
->addMetric(METRIC_SITES_OUTBOUND, $outbound)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_REQUESTS), 1)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_INBOUND), $inbound)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_OUTBOUND), $outbound);
|
||||
} else {
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
|
||||
}
|
||||
|
||||
$queueForStatsUsage->trigger();
|
||||
}
|
||||
}
|
||||
@@ -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])) {
|
||||
|
||||
@@ -84,6 +84,7 @@ class StatsUsage extends Event
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
'context' => $this->context,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -339,6 +339,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Update extends Action
|
||||
|
||||
$authenticator->setAttribute('verified', true);
|
||||
|
||||
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
|
||||
$dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true]));
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$factors = $session->getAttribute('factors', []);
|
||||
@@ -127,7 +127,7 @@ class Update extends Action
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$session->setAttribute('factors', $factors);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class Update extends Action
|
||||
$mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]);
|
||||
$mfaRecoveryCodes = \array_values($mfaRecoveryCodes);
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -147,11 +147,13 @@ class Update extends Action
|
||||
$factors[] = $type;
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$mfaUpdatedAt = DateTime::now();
|
||||
|
||||
$session
|
||||
->setAttribute('factors', $factors)
|
||||
->setAttribute('mfaUpdatedAt', DateTime::now());
|
||||
->setAttribute('mfaUpdatedAt', $mfaUpdatedAt);
|
||||
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
|
||||
@@ -93,7 +93,7 @@ class Create extends Action
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class Update extends Action
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class Update extends Action
|
||||
): void {
|
||||
$user->setAttribute('mfa', $mfa);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa]));
|
||||
|
||||
if ($mfa) {
|
||||
$factors = $session->getAttribute('factors', []);
|
||||
@@ -89,7 +89,7 @@ class Update extends Action
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$session->setAttribute('factors', $factors);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -143,7 +143,12 @@ class Base extends Action
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
@@ -249,7 +254,12 @@ class Base extends Action
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site);
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
|
||||
@@ -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').
|
||||
*/
|
||||
|
||||
+5
-2
@@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
|
||||
use Utopia\Database\Validator\Key;
|
||||
@@ -98,7 +99,8 @@ class Delete extends Action
|
||||
}
|
||||
|
||||
if ($attribute->getAttribute('status') === 'available') {
|
||||
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
|
||||
$attribute->setAttribute('status', 'deleting');
|
||||
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting']));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
|
||||
@@ -118,7 +120,8 @@ class Delete extends Action
|
||||
}
|
||||
|
||||
if ($relatedAttribute->getAttribute('status') === 'available') {
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
|
||||
$relatedAttribute->setAttribute('status', 'deleting');
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting']));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
|
||||
|
||||
@@ -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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
@@ -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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
|
||||
->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(
|
||||
|
||||
+9
-6
@@ -79,6 +79,7 @@ class Delete extends Action
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
@@ -95,6 +96,7 @@ class Delete extends Action
|
||||
?\DateTime $requestTimestamp,
|
||||
UtopiaResponse $response,
|
||||
Database $dbForProject,
|
||||
callable $getDatabasesDB,
|
||||
Event $queueForEvents,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
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
|
||||
);
|
||||
@@ -211,8 +214,8 @@ class Delete extends Action
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); // per collection
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1); // per collection
|
||||
|
||||
$response->addHeader('X-Debug-Operations', 1);
|
||||
|
||||
|
||||
+9
-6
@@ -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('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, StatsUsage $queueForStatsUsage, 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,16 +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);
|
||||
$document = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId, $queries);
|
||||
} elseif (! empty($selects)) {
|
||||
// has selects, allow relationship on documents!
|
||||
$document = $dbForProject->getDocument($collectionTableId, $documentId, $queries);
|
||||
$document = $dbForDatabases->getDocument($collectionTableId, $documentId, $queries);
|
||||
} else {
|
||||
// has no selects, disable relationship looping on documents!
|
||||
$document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument($collectionTableId, $documentId, $queries));
|
||||
$document = $dbForDatabases->skipRelationships(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId, $queries));
|
||||
}
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
@@ -131,8 +134,8 @@ class Get extends Action
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
->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('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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, StatsUsage $queueForStatsUsage, 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);
|
||||
|
||||
$queueForStatsUsage
|
||||
->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
|
||||
|
||||
+15
-11
@@ -87,6 +87,7 @@ class Upsert extends Action
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
@@ -95,7 +96,7 @@ class Upsert extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -124,6 +125,7 @@ class Upsert extends Action
|
||||
$data = $this->parseOperators($data, $collection);
|
||||
}
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
@@ -134,13 +136,15 @@ class Upsert extends Action
|
||||
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
// If no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario)
|
||||
if (\is_null($permissions)) {
|
||||
if ($transactionId !== null) {
|
||||
// Use transaction-aware document retrieval to see changes from same transaction
|
||||
$oldDocument = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
$oldDocument = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$oldDocument = $authorization->skip(fn () => $dbForProject->getDocument($collectionTableId, $documentId));
|
||||
$oldDocument = $authorization->skip(fn () => $dbForDatabases->getDocument($collectionTableId, $documentId));
|
||||
}
|
||||
if ($oldDocument->isEmpty()) {
|
||||
if (!empty($user->getId())) {
|
||||
@@ -182,7 +186,7 @@ class Upsert extends Action
|
||||
$newDocument = new Document($data);
|
||||
$operations = 0;
|
||||
|
||||
$setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations, $authorization) {
|
||||
$setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $dbForDatabases, $database, &$operations, $authorization) {
|
||||
$operations++;
|
||||
|
||||
$relationships = \array_filter(
|
||||
@@ -226,7 +230,7 @@ class Upsert extends Action
|
||||
if ($relation instanceof Document) {
|
||||
$relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser);
|
||||
|
||||
$oldDocument = $authorization->skip(fn () => $dbForProject->getDocument(
|
||||
$oldDocument = $authorization->skip(fn () => $dbForDatabases->getDocument(
|
||||
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
|
||||
$relation->getId()
|
||||
));
|
||||
@@ -257,8 +261,8 @@ class Upsert extends Action
|
||||
$setCollection($collection, $newDocument);
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), \max(1, $operations));
|
||||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
@@ -327,8 +331,8 @@ class Upsert extends Action
|
||||
|
||||
$upserted = [];
|
||||
try {
|
||||
$dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {
|
||||
return $dbForProject->upsertDocuments(
|
||||
$dbForDatabases->withPreserveDates(function () use (&$upserted, $dbForDatabases, $database, $collection, $newDocument) {
|
||||
return $dbForDatabases->upsertDocuments(
|
||||
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
[$newDocument],
|
||||
onNext: function (Document $document) use (&$upserted) {
|
||||
@@ -351,9 +355,9 @@ class Upsert extends Action
|
||||
if (empty($upserted[0])) {
|
||||
if ($transactionId !== null) {
|
||||
// For transactions, get the document with transaction changes applied
|
||||
$upserted[0] = $transactionState->getDocument($collectionTableId, $documentId, $transactionId);
|
||||
$upserted[0] = $transactionState->getDocument($database, $collectionTableId, $documentId, $transactionId);
|
||||
} else {
|
||||
$upserted[0] = $dbForProject->getDocument($collectionTableId, $documentId);
|
||||
$upserted[0] = $dbForDatabases->getDocument($collectionTableId, $documentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user