Merge remote-tracking branch 'origin/1.9.x' into fix-large-file-download

This commit is contained in:
Damodar Lohani
2026-04-16 01:16:51 +00:00
115 changed files with 4011 additions and 1347 deletions
@@ -0,0 +1,29 @@
# Patch Release Checklist for Appwrite
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
## Checklist
### Bump console image
Update the console Docker image tag in both files:
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
### Bump Appwrite version
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
### Update CHANGES.md
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
## Final review
- [ ] Ask user to review changes before commiting
- [ ] Ask user to update `CHANGES.md` with PRs
- [ ] Ask user to generate specs, if needed
- [ ] Ask user to add request and response filters, if needed
+1 -1
View File
@@ -512,7 +512,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
+4
View File
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
## Cross-repo context
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+5 -4
View File
@@ -2,10 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
@@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
@@ -9,8 +9,8 @@ return [
'key' => 'graphql',
'name' => 'GraphQL',
],
'realtime' => [
'key' => 'realtime',
'name' => 'Realtime',
'websocket' => [
'key' => 'websocket',
'name' => 'Websocket',
],
];
+4 -4
View File
@@ -137,7 +137,7 @@ return [
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -193,7 +193,7 @@ return [
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -235,7 +235,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/proxy',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
@@ -291,7 +291,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => false,
'optional' => true,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
+4 -4
View File
@@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
+1 -1
View File
@@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+4 -186
View File
@@ -71,202 +71,20 @@ Http::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatus',
description: '/docs/references/projects/update-service-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$services = $project->getAttribute('services', []);
$services[$service] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatusAll',
description: '/docs/references/projects/update-service-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
$services[$service] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/api')
->desc('Update API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$apis = $project->getAttribute('apis', []);
$apis[$api] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/api/all')
->desc('Update all API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allApis = array_keys(Config::getParam('apis'));
$apis = [];
foreach ($allApis as $api) {
$apis[$api] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/oauth2')
+18 -8
View File
@@ -7,9 +7,9 @@ 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\Publisher\Certificate;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -25,6 +25,7 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -32,6 +33,7 @@ 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\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -892,6 +894,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -916,6 +921,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
@@ -1006,11 +1014,11 @@ Http::init()
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('platform')
->inject('authorization')
->inject('certifiedDomains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
@@ -1036,7 +1044,7 @@ Http::init()
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
@@ -1092,10 +1100,11 @@ Http::init()
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: $document,
skipRenewCheck: true,
));
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
@@ -1419,6 +1428,7 @@ Http::error()
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
case 405: // Error allowed publicly
case 408: // Error allowed publicly
case 409: // Error allowed publicly
case 412: // Error allowed publicly
+37 -37
View File
@@ -3,15 +3,17 @@
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\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@@ -88,7 +90,7 @@ Http::init()
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('auditContext')
->inject('project')
->inject('user')
->inject('session')
@@ -97,7 +99,7 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
@@ -193,7 +195,7 @@ Http::init()
'name' => $apiKey->getName(),
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
// For standard keys, update last accessed time
@@ -264,7 +266,7 @@ Http::init()
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
default => ACTIVITY_TYPE_KEY_PROJECT,
});
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
// Apply permission
@@ -425,7 +427,7 @@ Http::init()
}
if (! empty($method)) {
$namespace = $method->getNamespace();
$namespace = \strtolower($method->getNamespace());
if (
array_key_exists($namespace, $project->getAttribute('services', []))
@@ -436,6 +438,15 @@ Http::init()
}
}
// Step 8b: Check REST protocol status
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
// Step 9: Validate scope permissions
$allowed = (array) $route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
@@ -477,7 +488,7 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -494,7 +505,7 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
@@ -511,14 +522,6 @@ Http::init()
default => '',
};
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
/*
* Abuse Check
*/
@@ -595,13 +598,12 @@ Http::init()
->setProject($project)
->setUser($user);
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
$auditContext->mode = $mode;
$auditContext->userAgent = $request->getUserAgent('');
$auditContext->ip = $request->getIP();
$auditContext->hostname = $request->getHostname();
$auditContext->event = $route->getLabel('audits.event', '');
$auditContext->project = $project;
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
@@ -610,7 +612,7 @@ Http::init()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
$auditContext->user = $userClone;
}
/* Auto-set projects */
@@ -789,7 +791,8 @@ Http::shutdown()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('auditContext')
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
@@ -806,7 +809,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -901,7 +904,7 @@ Http::shutdown()
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (! empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
$auditContext->resource = $resource;
}
}
@@ -911,8 +914,8 @@ Http::shutdown()
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
@@ -930,24 +933,21 @@ Http::shutdown()
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
$auditContext->payload = $responsePayload;
}
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
+1 -2
View File
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
@@ -533,6 +531,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
+1 -1
View File
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
+2 -2
View File
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.9.0';
const APP_CACHE_BUSTER = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
+15
View File
@@ -1,8 +1,11 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Utopia\Database\Documents\User;
@@ -81,6 +84,18 @@ $container->set('publisherMessaging', function (Publisher $publisher) {
$container->set('publisherWebhooks', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
+2 -12
View File
@@ -4,9 +4,8 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit as AuditEvent;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -14,7 +13,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
@@ -128,9 +126,6 @@ return function (Container $container): void {
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
@@ -149,18 +144,13 @@ return function (Container $container): void {
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('queueForAudits', function (Publisher $publisher) {
return new AuditEvent($publisher);
}, ['publisher']);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
-15
View File
@@ -1,8 +1,6 @@
<?php
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -10,7 +8,6 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -311,10 +308,6 @@ return function (Container $container): void {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
@@ -323,10 +316,6 @@ return function (Container $container): void {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
@@ -339,10 +328,6 @@ return function (Container $container): void {
return new Realtime();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
+4 -2
View File
@@ -653,9 +653,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$user = $connectionContainer->get('user'); /** @var User $user */
$logUser = $user;
$apis = $project->getAttribute('apis', []);
// Websocket is what to check, but realtime is checked too for backwards compatibility
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
+1 -1
View File
@@ -993,7 +993,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
<<: *x-logging
restart: unless-stopped
stop_signal: SIGINT
image: openruntimes/executor:0.7.22
image: openruntimes/executor:0.11.4
networks:
- appwrite
- runtimes
+1 -1
View File
@@ -92,7 +92,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},
Generated
+97 -83
View File
@@ -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": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "f6a87c1012b316e614258f8f57a28e48",
"packages": [
{
"name": "adhocore/jwt",
@@ -2887,16 +2887,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
@@ -2948,7 +2948,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
},
"funding": [
{
@@ -2968,20 +2968,20 @@
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php82",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php82.git",
"reference": "5d2ed36f7734637dacc025f179698031951b1692"
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
"reference": "5d2ed36f7734637dacc025f179698031951b1692",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59",
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59",
"shasum": ""
},
"require": {
@@ -3028,7 +3028,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
},
"funding": [
{
@@ -3048,20 +3048,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
"shasum": ""
},
"require": {
@@ -3108,7 +3108,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
},
"funding": [
{
@@ -3128,20 +3128,20 @@
"type": "tidelift"
}
],
"time": "2025-07-08T02:45:35+00:00"
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php85",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"shasum": ""
},
"require": {
@@ -3188,7 +3188,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
},
"funding": [
{
@@ -3208,7 +3208,7 @@
"type": "tidelift"
}
],
"time": "2025-06-23T16:12:55+00:00"
"time": "2026-04-10T16:50:15+00:00"
},
{
"name": "symfony/service-contracts",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.19",
"version": "5.3.21",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.19"
"source": "https://github.com/utopia-php/database/tree/5.3.21"
},
"time": "2026-03-31T15:52:08+00:00"
"time": "2026-04-10T12:38:57+00:00"
},
{
"name": "utopia-php/detector",
@@ -4271,16 +4271,16 @@
},
{
"name": "utopia-php/http",
"version": "0.34.19",
"version": "0.34.20",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c"
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/995c119f31866cacd42d63b1f922bf86eabb396c",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
"shasum": ""
},
"require": {
@@ -4319,9 +4319,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.19"
"source": "https://github.com/utopia-php/http/tree/0.34.20"
},
"time": "2026-04-08T10:23:17+00:00"
"time": "2026-04-12T14:25:22+00:00"
},
{
"name": "utopia-php/image",
@@ -5225,16 +5225,16 @@
},
{
"name": "utopia-php/vcs",
"version": "3.1.0",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3",
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3",
"shasum": ""
},
"require": {
@@ -5268,9 +5268,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
"source": "https://github.com/utopia-php/vcs/tree/3.2.0"
},
"time": "2026-03-24T08:49:14+00:00"
"time": "2026-04-08T16:00:31+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5381,38 +5381,48 @@
},
{
"name": "webonyx/graphql-php",
"version": "v14.11.10",
"version": "v15.31.5",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8"
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.3",
"doctrine/coding-standard": "^6.0",
"nyholm/psr7": "^1.2",
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.94.2",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "0.12.82",
"phpstan/phpstan-phpunit": "0.12.18",
"phpstan/phpstan-strict-rules": "0.12.9",
"phpunit/phpunit": "^7.2 || ^8.5",
"psr/http-message": "^1.0",
"react/promise": "2.*",
"simpod/php-coveralls-mirror": "^3.0"
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.46",
"phpstan/phpstan-phpunit": "2.0.16",
"phpstan/phpstan-strict-rules": "2.0.10",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.0 || ^3.0",
"rector/rector": "^2.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
"ticketswap/phpstan-error-formatter": "1.3.0"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
@@ -5434,30 +5444,34 @@
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
},
"funding": [
{
"url": "https://github.com/spawnia",
"type": "github"
},
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2023-07-05T14:23:37+00:00"
"time": "2026-04-11T18:06:15+00:00"
}
],
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.17.7",
"version": "1.17.11",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
"shasum": ""
},
"require": {
@@ -5493,9 +5507,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.7"
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.11"
},
"time": "2026-04-08T08:51:05+00:00"
"time": "2026-04-11T02:42:32+00:00"
},
{
"name": "brianium/paratest",
@@ -7764,16 +7778,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
@@ -7823,7 +7837,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
},
"funding": [
{
@@ -7843,20 +7857,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"shasum": ""
},
"require": {
@@ -7905,7 +7919,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
},
"funding": [
{
@@ -7925,11 +7939,11 @@
"type": "tidelift"
}
],
"time": "2025-06-27T09:58:17+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -7990,7 +8004,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
},
"funding": [
{
@@ -8014,7 +8028,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -8070,7 +8084,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
},
"funding": [
{
+1
View File
@@ -1288,6 +1288,7 @@ services:
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:
+22 -2
View File
@@ -7,6 +7,8 @@ use Appwrite\Event\Message\Execution as ExecutionMessage;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -30,9 +32,27 @@ class Log extends Listener
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
{
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
project: new Document($event->project),
execution: new Document($event->execution),
project: $project,
execution: $execution,
));
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Context;
use Utopia\Database\Document;
class Audit
{
public function __construct(
public ?Document $project = null,
public ?Document $user = null,
public string $mode = '',
public string $userAgent = '',
public string $ip = '',
public string $hostname = '',
public string $event = '',
public string $resource = '',
public array $payload = [],
) {
}
public function isEmpty(): bool
{
return $this->project === null
&& $this->user === null
&& $this->mode === ''
&& $this->userAgent === ''
&& $this->ip === ''
&& $this->hostname === ''
&& $this->event === ''
&& $this->resource === ''
&& $this->payload === [];
}
}
+40 -21
View File
@@ -637,9 +637,11 @@ class Event
*/
$eventValues = \array_values($events);
/**
* Return a combined list of table, collection events and if tablesdb present then include all for backward compatibility
*/
$databaseType = $database?->getAttribute('type', 'legacy');
if ($database !== null && !\in_array($databaseType, ['legacy', 'tablesdb'], true)) {
return $eventValues;
}
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
}
@@ -662,21 +664,30 @@ class Event
}
/**
* Adds `table` events for `collection` events.
* Adds table/collection counterpart events for backward compatibility.
*
* Example:
*
* `databases.*.collections.*.documents.*.update` →\
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
*
* `databases.*.tables.*.rows.*.update` →\
* `[databases.*.tables.*.rows.*.update, databases.*.collections.*.documents.*.update]`
*/
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
{
$tableEventMap = [
$collectionsToTablesMap = [
'documents' => 'rows',
'collections' => 'tables',
'attributes' => 'columns',
];
$tablesToCollectionsMap = [
'rows' => 'documents',
'tables' => 'collections',
'columns' => 'attributes',
];
$databasesEventMap = [
'tablesdb' => 'databases',
'tables' => 'collections',
@@ -687,7 +698,10 @@ class Event
if (
(
str_contains($pattern, 'databases.') &&
str_contains($firstEvent, 'collections')
(
str_contains($firstEvent, 'collections') ||
str_contains($firstEvent, 'tables')
)
) ||
(
str_contains($firstEvent, 'tablesdb.')
@@ -698,25 +712,16 @@ class Event
$pairedEvents[] = $event;
// tablesdb needs databases event with tables and collections
if (str_contains($event, 'tablesdb')) {
$databasesSideEvent = str_replace(
array_keys($databasesEventMap),
array_values($databasesEventMap),
$event
);
$databasesSideEvent = self::replaceEventSegments($event, $databasesEventMap);
$pairedEvents[] = $databasesSideEvent;
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$databasesSideEvent
);
$tableSideEvent = self::replaceEventSegments($databasesSideEvent, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'collections')) {
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$event
);
$tableSideEvent = self::replaceEventSegments($event, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'tables')) {
$collectionSideEvent = self::replaceEventSegments($event, $tablesToCollectionsMap);
$pairedEvents[] = $collectionSideEvent;
}
}
@@ -728,6 +733,20 @@ class Event
return array_values(array_unique($events));
}
/**
* Replace only exact event path segments, never partial substrings.
*/
private static function replaceEventSegments(string $event, array $map): string
{
$parts = \explode('.', $event);
$parts = \array_map(
fn (string $part) => $map[$part] ?? $part,
$parts
);
return \implode('.', $parts);
}
/**
* Maps event terminology based on database type
*/
+19
View File
@@ -53,4 +53,23 @@ class Execution extends Event
'execution' => $this->execution,
];
}
/**
* Trim payload for the execution event.
* Only the project ID is needed — the worker DI fetches the full project from the platform database.
*
* @return array
*/
protected function trimPayload(): array
{
$trimmed = [];
if ($this->project) {
$trimmed['project'] = new Document([
'$id' => $this->project->getId(),
]);
}
return $trimmed;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Event\Message;
use Appwrite\Event\Context\Audit as AuditContext;
use Utopia\Database\Document;
final class Audit extends Base
{
public function __construct(
public readonly string $event,
public readonly array $payload,
public readonly Document $project = new Document(),
public readonly Document $user = new Document(),
public readonly string $resource = '',
public readonly string $mode = '',
public readonly string $ip = '',
public readonly string $userAgent = '',
public readonly string $hostname = '',
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'user' => $this->user->getArrayCopy(),
'payload' => $this->payload,
'resource' => $this->resource,
'mode' => $this->mode,
'ip' => $this->ip,
'userAgent' => $this->userAgent,
'event' => $this->event,
'hostname' => $this->hostname,
];
}
public static function fromArray(array $data): static
{
return new self(
event: $data['event'] ?? '',
payload: $data['payload'] ?? [],
project: new Document($data['project'] ?? []),
user: new Document($data['user'] ?? []),
resource: $data['resource'] ?? '',
mode: $data['mode'] ?? '',
ip: $data['ip'] ?? '',
userAgent: $data['userAgent'] ?? '',
hostname: $data['hostname'] ?? '',
);
}
public static function fromContext(AuditContext $context): static
{
return new self(
event: $context->event,
payload: $context->payload,
project: $context->project ?? new Document(),
user: $context->user ?? new Document(),
resource: $context->resource,
mode: $context->mode,
ip: $context->ip,
userAgent: $context->userAgent,
hostname: $context->hostname,
);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Certificate extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $domain,
public readonly bool $skipRenewCheck = false,
public readonly ?string $validationDomain = null,
public readonly string $action = \Appwrite\Event\Certificate::ACTION_GENERATION,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'domain' => $this->domain->getArrayCopy(),
'skipRenewCheck' => $this->skipRenewCheck,
'validationDomain' => $this->validationDomain,
'action' => $this->action,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
domain: new Document($data['domain'] ?? []),
skipRenewCheck: $data['skipRenewCheck'] ?? false,
validationDomain: $data['validationDomain'] ?? null,
action: $data['action'] ?? \Appwrite\Event\Certificate::ACTION_GENERATION,
);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Screenshot extends Base
{
public function __construct(
public readonly Document $project,
public readonly string $deploymentId,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'deploymentId' => $this->deploymentId,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
deploymentId: $data['deploymentId'] ?? '',
);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Audit as AuditMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Audit extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(AuditMessage $message): string|bool
{
// Audit delivery is best-effort and should never fail the request lifecycle.
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[Audit] Failed to publish audit message: ' . $th->getMessage());
return false;
}
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Certificate as CertificateMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Certificate extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(CertificateMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Screenshot as ScreenshotMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Screenshot extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(ScreenshotMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
+20
View File
@@ -61,6 +61,26 @@ class Realtime extends Event
return $this->subscribers;
}
/**
* Reset the event state for long-lived worker processes.
*
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
* to prevent stale state from affecting subsequent triggers.
*/
public function reset(): self
{
parent::reset();
$this->subscribers = [];
$this->context = [];
$this->project = null;
$this->user = null;
$this->userId = null;
return $this;
}
/**
* Execute Event.
*
+2 -2
View File
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
/**
* Create a new promise that resolves when all passed in promises resolve.
*
* @param array $promisesOrValues
* @param iterable $promisesOrValues
* @return GQLPromise
*/
abstract public function all(array $promisesOrValues): GQLPromise;
abstract public function all(iterable $promisesOrValues): GQLPromise;
}
@@ -35,8 +35,12 @@ class Swoole extends Adapter
return new GQLPromise($promise, $this);
}
public function all(array $promisesOrValues): GQLPromise
public function all(iterable $promisesOrValues): GQLPromise
{
if ($promisesOrValues instanceof \Traversable) {
$promisesOrValues = \iterator_to_array($promisesOrValues);
}
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\GraphQL;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
final class ResolverLock
{
public Channel $channel;
public ?int $owner = null;
public int $depth = 0;
public function __construct()
{
$this->channel = new Channel(1);
}
/**
* Acquire the lock. Re-entering from the same coroutine only
* increments depth to avoid self-deadlock.
*/
public function acquire(): void
{
$cid = Coroutine::getCid();
if ($this->owner === $cid) {
$this->depth++;
return;
}
$this->channel->push(true);
$this->owner = $cid;
$this->depth = 1;
}
/**
* Release the lock.
*/
public function release(): void
{
if ($this->owner !== Coroutine::getCid()) {
return;
}
$this->depth--;
if ($this->depth > 0) {
return;
}
$this->owner = null;
$this->channel->pop();
}
}
+237 -107
View File
@@ -6,6 +6,7 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@@ -13,6 +14,75 @@ use Utopia\System\System;
class Resolvers
{
/**
* Request-scoped locks keyed by the per-request GraphQL Http instance.
*
* @var array<string, ResolverLock>
*/
private static array $locks = [];
/**
* Preserve response side effects that callers depend on, such as session
* cookies created by account auth routes.
*/
private static function mergeResponseSideEffects(Response $from, Response $to): void
{
foreach ($from->getCookies() as $cookie) {
$to->removeCookie($cookie['name']);
$to->addCookie(
$cookie['name'],
$cookie['value'],
$cookie['expire'],
$cookie['path'],
$cookie['domain'],
$cookie['secure'],
$cookie['httponly'],
$cookie['samesite']
);
}
$headers = $from->getHeaders();
$fallbackCookies = $headers['X-Fallback-Cookies'] ?? null;
if ($fallbackCookies === null) {
return;
}
$to->removeHeader('X-Fallback-Cookies');
foreach ((array) $fallbackCookies as $value) {
$to->addHeader('X-Fallback-Cookies', $value);
}
}
/**
* Get the current request container.
*/
private static function getResolverContainer(Http $utopia): Container
{
$container = $utopia->getResource('container');
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
/** @var Container $container */
return $container;
}
/** @var callable(): Container $container */
return $container();
}
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
*/
private static function getLock(Http $utopia): ResolverLock
{
$key = \spl_object_hash($utopia);
if (!isset(self::$locks[$key])) {
self::$locks[$key] = new ResolverLock();
}
return self::$locks[$key];
}
/**
* Create a resolver for a given API {@see Route}.
*
@@ -24,34 +94,39 @@ class Resolvers
Http $utopia,
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($route, $args): void {
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
);
});
}
/**
@@ -91,18 +166,23 @@ class Resolvers
string $collectionId,
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -122,23 +202,29 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
$beforeResolve = function ($payload) {
return $payload['documents'];
};
$beforeResolve = function ($payload) {
return $payload['documents'];
};
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
beforeResolve: $beforeResolve,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -158,19 +244,24 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -190,19 +281,24 @@ class Resolvers
callable $url,
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -220,18 +316,23 @@ class Resolvers
string $collectionId,
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -241,7 +342,7 @@ class Resolvers
* @param callable $resolve
* @param callable $reject
* @param callable|null $beforeResolve
* @param callable|null $beforeReject
* @param callable|null $prepareRequest
* @return void
* @throws Exception
*/
@@ -252,38 +353,67 @@ class Resolvers
callable $resolve,
callable $reject,
?callable $beforeResolve = null,
?callable $beforeReject = null,
?callable $prepareRequest = null,
): void {
// Drop json content type so post args are used directly
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
$lock = self::getLock($utopia);
$request = clone $request;
$utopia->setResource('request', static fn () => $request);
$response->setContentType(Response::CONTENT_TYPE_NULL);
$lock->acquire();
$original = $utopia->getRoute();
try {
$route = $utopia->match($request, fresh: true);
$request = clone $request;
$utopia->execute($route, $request);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
// Drop json content type so post args are used directly.
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
if ($prepareRequest) {
$prepareRequest($request);
}
/** @var Response $resolverResponse */
$resolverResponse = clone $utopia->getResource('response');
$container = self::getResolverContainer($utopia);
$container->set('request', static fn () => $request);
$container->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
$route = $utopia->match($request, fresh: true);
$request->setRoute($route);
$utopia->execute($route, $request, $resolverResponse);
self::mergeResponseSideEffects($resolverResponse, $response);
if ($resolverResponse->isSent()) {
$response
->setStatusCode($resolverResponse->getStatusCode())
->setSent(true);
$resolve(null);
return;
}
$payload = $resolverResponse->getPayload();
$statusCode = $resolverResponse->getStatusCode();
} catch (\Throwable $e) {
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
$lock->release();
unset(self::$locks[\spl_object_hash($utopia)]);
}
$payload = $response->getPayload();
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
if ($beforeReject) {
$payload = $beforeReject($payload);
}
if ($statusCode < 200 || $statusCode >= 400) {
$reject(new GQLException(
message: $payload['message'],
code: $response->getStatusCode()
code: $statusCode
));
return;
}
+8 -3
View File
@@ -3,12 +3,13 @@
namespace Appwrite\GraphQL\Types;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Assoc extends Json
{
public $name = 'Assoc';
public $description = 'The `Assoc` scalar type represents associative array values.';
public string $name = 'Assoc';
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
public function serialize($value)
{
@@ -30,6 +31,10 @@ class Assoc extends Json
public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return \json_decode($valueNode->value, true);
if ($valueNode instanceof StringValueNode) {
return \json_decode($valueNode->value, true);
}
return parent::parseLiteral($valueNode, $variables);
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
class InputFile extends ScalarType
{
public $name = 'InputFile';
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
public string $name = 'InputFile';
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
public function serialize($value)
+2 -2
View File
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Json extends ScalarType
{
public $name = 'Json';
public $description = 'The `JSON` scalar type represents JSON values as specified by
public string $name = 'Json';
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
public function serialize($value)
+1
View File
@@ -93,6 +93,7 @@ abstract class Migration
'1.8.0' => 'V23',
'1.8.1' => 'V23',
'1.9.0' => 'V24',
'1.9.1' => 'V24',
];
/**
@@ -50,6 +50,11 @@ class Create extends Action
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return false;
}
public function __construct()
{
$this
@@ -139,30 +144,42 @@ 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, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
$hasData = !empty($data);
$hasDocuments = !empty($documents);
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// When empty documents are supported, an empty payload should still be treated as single create.
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
$data = [];
$hasData = true;
}
if ($hasData && $hasDocuments) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
}
if (!empty($data) && empty($documentId)) {
if ($hasData && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
if ($hasDocuments && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
@@ -170,13 +187,13 @@ class Create extends Action
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
if ($hasDocuments && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
$isBulk = $hasDocuments;
if ($hasData) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
@@ -145,7 +145,11 @@ class XList extends Action
$documentsField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_DOCUMENTS);
$documentsCacheHit = false;
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
try {
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
} catch (\Throwable) {
$cachedDocuments = null;
}
if ($cachedDocuments !== null &&
$cachedDocuments !== false &&
@@ -157,21 +161,30 @@ class XList extends Action
} else {
$documents = $find();
// Convert Document objects to arrays for caching
$documentsArray = \array_map(function ($doc) {
return $doc->getArrayCopy();
}, $documents);
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
try {
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
} catch (\Throwable) {
}
}
if ($includeTotal) {
$totalField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_TOTAL);
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
try {
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
} catch (\Throwable) {
$cachedTotal = null;
}
if ($cachedTotal !== null && $cachedTotal !== false) {
$total = $cachedTotal;
} else {
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
try {
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
} catch (\Throwable) {
}
}
} else {
$total = 0;
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listDocumentsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/documentsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listDocumentsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/documentsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listVectorsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/vectorsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,57 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listVectorsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/vectorsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
$service->addAction(UpdateTable::getName(), new UpdateTable());
$service->addAction(DeleteTable::getName(), new DeleteTable());
$service->addAction(ListTables::getName(), new ListTables());
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
}
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
$service->addAction(DeleteRow::getName(), new DeleteRow());
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
@@ -69,7 +67,6 @@ class VectorsDB extends Base
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
$service->addAction(ListCollections::getName(), new ListCollections());
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
}
@@ -92,7 +89,6 @@ class VectorsDB extends Base
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
}
private function registerTransactionActions(Service $service): void
@@ -650,26 +650,30 @@ class Databases extends Action
Document|null $attribute = null,
Document|null $index = null,
): void {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
try {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
}
$queueForRealtime->trigger();
} finally {
$queueForRealtime->reset();
}
$queueForRealtime->trigger();
}
}
@@ -6,9 +6,9 @@ use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
@@ -58,7 +58,7 @@ class Builds extends Action
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
@@ -84,7 +84,7 @@ class Builds extends Action
Document $project,
Database $dbForPlatform,
Event $queueForEvents,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -126,7 +126,7 @@ class Builds extends Action
$deviceForFunctions,
$deviceForSites,
$deviceForFiles,
$queueForScreenshots,
$publisherForScreenshots,
$queueForWebhooks,
$queueForFunctions,
$queueForRealtime,
@@ -161,7 +161,7 @@ class Builds extends Action
Device $deviceForFunctions,
Device $deviceForSites,
Device $deviceForFiles,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -1120,10 +1120,10 @@ class Builds extends Action
/** Screenshot site */
if ($resource->getCollection() === 'sites') {
$queueForScreenshots
->setDeploymentId($deployment->getId())
->setProject($project)
->trigger();
$publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot(
project: $project,
deploymentId: $deployment->getId(),
));
Console::log('Site screenshot queued');
}
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Message\Screenshot;
use Appwrite\Event\Realtime;
use Appwrite\Permission;
use Appwrite\Role;
@@ -62,9 +63,11 @@ class Screenshots extends Action
throw new \Exception('Missing payload');
}
$screenshotMessage = Screenshot::fromArray($payload);
Console::log('Site screenshot started');
$deploymentId = $payload['deploymentId'] ?? null;
$deploymentId = $screenshotMessage->deploymentId;
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits;
use Appwrite\Event\Audit;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$size = $publisherForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Certificate $queueForCertificates, Response $response): void
public function action(int|string $threshold, Certificate $publisherForCertificates, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForCertificates->getSize();
$size = $publisherForCertificates->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -2,19 +2,19 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
@@ -75,17 +75,17 @@ class Get extends Base
->inject('response')
->inject('queueForDatabase')
->inject('queueForDeletes')
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('queueForMails')
->inject('queueForFunctions')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('publisherForMigrations')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->callback($this->action(...));
}
@@ -95,32 +95,32 @@ class Get extends Base
Response $response,
Database $queueForDatabase,
Delete $queueForDeletes,
Audit $queueForAudits,
Audit $publisherForAudits,
Mail $queueForMails,
Func $queueForFunctions,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Build $queueForBuilds,
Messaging $queueForMessaging,
MigrationPublisher $publisherForMigrations,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
): void {
$threshold = (int) $threshold;
$queue = match ($name) {
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates,
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
};
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs;
use Appwrite\Event\Audit;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForAudits')
->inject('publisherForAudits')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForAudits->getSize();
$size = $publisherForAudits->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -62,7 +62,7 @@ class Create extends Base
))
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('queueForEvents')
@@ -72,13 +72,10 @@ class Create extends Base
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
@@ -95,7 +92,7 @@ class Create extends Base
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes ?? [],
'scopes' => $scopes,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
@@ -60,7 +60,7 @@ class Update extends Base
))
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('queueForEvents')
@@ -70,13 +70,10 @@ class Update extends Base
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
@@ -92,7 +89,7 @@ class Update extends Base
$updates = new Document([
'name' => $name,
'scopes' => $scopes ?? [],
'scopes' => $scopes,
'expire' => $expire,
]);
@@ -64,7 +64,7 @@ class Create extends Action
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('request')
@@ -56,7 +56,7 @@ class Update extends Action
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('response')
->inject('queueForEvents')
@@ -0,0 +1,85 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectProtocolStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/protocols/:protocolId/status')
->httpAlias('/v1/projects/:projectId/api')
->desc('Update project protocol status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'protocols.[protocolId].update')
->label('audits.event', 'project.protocols.[protocolId].update')
->label('audits.resource', 'project.protocols/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateProtocolStatus',
description: <<<EOT
Update the status of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('protocolId', '', new WhiteList(array_keys(Config::getParam('protocols')), true), 'Protocol name. Can be one of: ' . \implode(', ', array_keys(Config::getParam('protocols'))))
->param('enabled', null, new Boolean(), 'Protocol status.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $protocolId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
Event $queueForEvents,
): void {
$protocols = $project->getAttribute('apis', []);
$protocols[$protocolId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'apis' => $protocols,
])));
$queueForEvents->setParam('protocolId', $protocolId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,85 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
use Appwrite\Event\Event;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectServiceStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/services/:serviceId/status')
->httpAlias('/v1/projects/:projectId/service')
->desc('Update project service status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'services.[serviceId].update')
->label('audits.event', 'project.services.[serviceId].update')
->label('audits.resource', 'project.services/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateServiceStatus',
description: <<<EOT
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('serviceId', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name. Can be one of: '.\implode(', ', array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']))))
->param('enabled', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $serviceId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
Event $queueForEvents
): void {
$services = $project->getAttribute('services', []);
$services[$serviceId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'services' => $services,
])));
$queueForEvents->setParam('serviceId', $serviceId);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -22,6 +22,8 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status\Update as UpdateProjectProtocolStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Services\Status\Update as UpdateProjectServiceStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@@ -40,6 +42,8 @@ class Http extends Service
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectProtocolStatus::getName(), new UpdateProjectProtocolStatus());
$this->addAction(UpdateProjectServiceStatus::getName(), new UpdateProjectServiceStatus());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@@ -62,7 +62,7 @@ class Create extends Action
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('platform')
@@ -70,7 +70,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@@ -114,13 +114,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@@ -66,7 +66,7 @@ class Create extends Action
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@@ -75,7 +75,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@@ -132,13 +132,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@@ -69,7 +69,7 @@ class Create extends Action
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@@ -78,7 +78,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@@ -136,13 +136,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@@ -66,7 +66,7 @@ class Create extends Action
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
@@ -75,7 +75,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
$this->validateDomainRestrictions($domain, $platform);
@@ -132,13 +132,14 @@ class Create extends Action
}
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Proxy\Action;
use Appwrite\SDK\AuthType;
@@ -56,7 +56,7 @@ class Update extends Action
))
->param('ruleId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Rule ID.', false, ['dbForProject'])
->inject('response')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
@@ -67,7 +67,7 @@ class Update extends Action
public function action(
string $ruleId,
Response $response,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Event $queueForEvents,
Document $project,
Database $dbForPlatform,
@@ -110,12 +110,13 @@ class Update extends Action
}
// Issue a TLS certificate when DNS verification is successful
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $project,
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->trigger();
]),
));
if (!empty($certificate)) {
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
@@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -65,9 +66,10 @@ class Create extends Action
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -85,9 +87,10 @@ class Create extends Action
string $name,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -104,10 +107,10 @@ class Create extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'signatureKey' => \bin2hex(\random_bytes(64)),
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
'enabled' => $enabled,
]);
@@ -72,6 +72,8 @@ class Get extends Action
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
@@ -15,6 +15,8 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
@@ -22,15 +24,15 @@ class Update extends Action
public static function getName()
{
return 'updateWebhookSignature';
return 'updateWebhookSecret';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/webhooks/:webhookId/signature')
->setHttpPath('/v1/webhooks/:webhookId/secret')
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature')
->desc('Update webhook signature key')
->desc('Update webhook secret key')
->groups(['api', 'webhooks'])
->label('scope', 'webhooks.write')
->label('event', 'webhooks.[webhookId].update')
@@ -39,9 +41,9 @@ class Update extends Action
->label('sdk', new Method(
namespace: 'webhooks',
group: null,
name: 'updateSignature',
name: 'updateSecret',
description: <<<EOT
Update the webhook signature key. This endpoint can be used to regenerate the signature key used to sign and validate payload deliveries for a specific webhook.
Update the webhook signing key. This endpoint can be used to regenerate the signing key used to sign and validate payload deliveries for a specific webhook.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
@@ -52,6 +54,7 @@ class Update extends Action
]
))
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -62,6 +65,7 @@ class Update extends Action
public function action(
string $webhookId,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -78,7 +82,7 @@ class Update extends Action
}
$updates = new Document([
'signatureKey' => \bin2hex(\random_bytes(64)),
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
]);
$webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
@@ -63,9 +63,9 @@ class Update extends Action
->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -80,9 +80,9 @@ class Update extends Action
string $url,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -102,9 +102,9 @@ class Update extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'enabled' => $enabled,
]);
@@ -118,6 +118,8 @@ class Update extends Action
$queueForEvents->setParam('webhookId', $webhook->getId());
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -78,6 +78,15 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
foreach ($queries as $query) {
$attribute = $query->getAttribute();
if ($attribute === 'authUsername') {
$query->setAttribute('httpUser');
} elseif ($attribute === 'tls') {
$query->setAttribute('security');
}
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
@@ -111,6 +120,10 @@ class XList extends Action
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
foreach ($webhooks as $webhook) {
$webhook->removeAttribute('signatureKey');
}
$response->dynamic(new Document([
'webhooks' => $webhooks,
'total' => $total,
@@ -6,7 +6,7 @@ use Appwrite\Platform\Modules\Webhooks\Http\Init;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Create as CreateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Delete as DeleteWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Get as GetWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature\Update as UpdateWebhookSignature;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret\Update as UpdateWebhookSecret;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Update as UpdateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\XList as ListWebhooks;
use Utopia\Platform\Service;
@@ -26,6 +26,6 @@ class Http extends Service
$this->addAction(GetWebhook::getName(), new GetWebhook());
$this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
$this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
$this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
$this->addAction(UpdateWebhookSecret::getName(), new UpdateWebhookSecret());
}
}
+19 -15
View File
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use DateTime;
use Swoole\Coroutine\Channel;
use Swoole\Process;
@@ -29,16 +29,16 @@ class Interval extends Action
->desc('Schedules tasks on regular intervals by publishing them to our queues')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->callback($this->action(...));
}
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): void
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): void
{
Console::title('Interval V1');
Console::success(APP_NAME . ' interval process v1 has started');
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $queueForCertificates);
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $publisherForCertificates);
$chan = new Channel(1);
Process::signal(SIGTERM, function () use ($chan) {
@@ -52,16 +52,16 @@ class Interval extends Action
}
}
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): array
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): array
{
$timers = [];
$tasks = $this->getTasks();
foreach ($tasks as $task) {
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $queueForCertificates) {
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $publisherForCertificates) {
$taskName = $task['name'];
Span::init("interval.{$taskName}");
try {
$task['callback']($dbForPlatform, $getProjectDB, $queueForCertificates);
$task['callback']($dbForPlatform, $getProjectDB, $publisherForCertificates);
} catch (\Exception $e) {
Span::error($e);
} finally {
@@ -80,15 +80,15 @@ class Interval extends Action
return [
[
'name' => 'domainVerification',
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates) {
$this->verifyDomain($dbForPlatform, $queueForCertificates);
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates) {
$this->verifyDomain($dbForPlatform, $publisherForCertificates);
},
'interval' => $intervalDomainVerification * 1000,
]
];
}
private function verifyDomain(Database $dbForPlatform, Certificate $queueForCertificates): void
private function verifyDomain(Database $dbForPlatform, Certificate $publisherForCertificates): void
{
$time = DatabaseDateTime::now();
$fromTime = new DateTime('-3 days'); // Max 3 days old
@@ -115,13 +115,17 @@ class Interval extends Action
foreach ($rules as $rule) {
try {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_DOMAIN_VERIFICATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION,
));
$processed++;
} catch (\Throwable $th) {
$failed++;
+15 -11
View File
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Publisher\Certificate;
use DateInterval;
use DateTime;
use Utopia\Console;
@@ -29,12 +29,12 @@ class Maintenance extends Action
->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.')
->inject('dbForPlatform')
->inject('console')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('queueForDeletes')
->callback($this->action(...));
}
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@@ -59,7 +59,7 @@ class Maintenance extends Action
$delay = $next->getTimestamp() - $now->getTimestamp();
}
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) {
$time = DatabaseDateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
@@ -92,7 +92,7 @@ class Maintenance extends Action
->trigger();
$this->notifyDeleteConnections($queueForDeletes);
$this->renewCertificates($dbForPlatform, $queueForCertificates);
$this->renewCertificates($dbForPlatform, $publisherForCertificates);
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
$this->notifyDeleteCSVExports($queueForDeletes);
@@ -124,7 +124,7 @@ class Maintenance extends Action
->trigger();
}
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void
{
$time = DatabaseDateTime::now();
@@ -158,13 +158,17 @@ class Maintenance extends Action
continue;
}
$queueForCertificate
->setDomain(new Document([
$publisherForCertificate->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
}
}
+17 -18
View File
@@ -622,29 +622,28 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$repo->execute('config', 'advice.defaultBranchName', 'false');
$repo->addRemote('origin', $gitUrl);
// Fetch and checkout base branch (or create if new repo)
// Fetch and checkout the target branch (e.g. dev) if it exists on remote,
// otherwise create it from the base branch (e.g. main).
// We build on top of the existing remote branch so a regular push
// works without force-pushing against protected branches.
$hasBranch = false;
try {
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $gitBranch);
$hasBranch = true;
} catch (\Throwable) {
// Branch doesn't exist on remote yet
}
if ($hasBranch) {
$repo->execute('checkout', '-f', $gitBranch);
} else {
// Fetch base branch to create the target branch from it
try {
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
$repo->execute('checkout', '-f', $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
try {
$repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags');
} catch (\Throwable) {
}
// Create or checkout dev branch from the base branch
// This ensures dev always starts from the latest base branch,
// avoiding history divergence caused by squash merges.
try {
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $gitBranch);
}
@@ -685,7 +684,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return true;
}
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable $e) {
Console::warning(" Git push failed: " . $e->getMessage());
return false;
+10 -9
View File
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Publisher\Certificate;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -29,11 +29,11 @@ class SSL extends Action
->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->callback($this->action(...));
}
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates): void
{
$domain = new Domain(!empty($domain) ? $domain : '');
if (!$domain->isKnown() || $domain->isTest()) {
@@ -98,12 +98,13 @@ class SSL extends Action
Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get());
}
$queueForCertificates
->setDomain(new Document([
'domain' => $domain->get()
]))
->setSkipRenewCheck($skipCheck)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: new Document([
'domain' => $domain->get(),
]),
skipRenewCheck: $skipCheck,
));
Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get());
}
@@ -7,6 +7,8 @@ use Cron\CronExpression;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Span\Span;
use Utopia\System\System;
/**
* ScheduleFunctions
@@ -88,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
return;
continue;
}
$schedule = $this->schedules[$scheduleKey];
@@ -102,8 +104,22 @@ class ScheduleFunctions extends ScheduleBase
->setFunction($schedule['resource'])
->setMethod('POST')
->setPath('/')
->setProject($schedule['project'])
->trigger();
->setProject($schedule['project']);
$projectDoc = $schedule['project'];
$functionDoc = $schedule['resource'];
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
Span::init('execution.trace.v1_functions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $projectDoc->getId());
Span::add('functionId', $functionDoc->getId());
Span::add('scheduleId', $schedule['$id'] ?? '');
Span::current()?->finish();
}
$queueForFunctions->trigger();
$this->recordEnqueueDelay($delayConfig['nextDate']);
}
+18 -17
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Message\Audit;
use Exception;
use Throwable;
use Utopia\Console;
@@ -40,7 +41,6 @@ class Audits extends Action
$this
->desc('Audits worker')
->inject('message')
->inject('project')
->inject('getAudit')
->callback($this->action(...));
@@ -50,14 +50,13 @@ class Audits extends Action
/**
* @param Message $message
* @param Document $project
* @param callable(Document): \Utopia\Audit\Audit $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Structure
*/
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
public function action(Message $message, callable $getAudit): Commit|NoCommit
{
$payload = $message->getPayload() ?? [];
@@ -65,19 +64,21 @@ class Audits extends Action
throw new Exception('Missing payload');
}
$auditMessage = Audit::fromArray($payload);
Console::info('Aggregating audit logs');
$event = $payload['event'] ?? '';
$event = $auditMessage->event;
$auditPayload = '';
if ($project->getId() === 'console') {
$auditPayload = $payload['payload'] ?? '';
if ($auditMessage->project->getId() === 'console') {
$auditPayload = $auditMessage->payload;
}
$mode = $payload['mode'] ?? '';
$resource = $payload['resource'] ?? '';
$userAgent = $payload['userAgent'] ?? '';
$ip = $payload['ip'] ?? '';
$user = new Document($payload['user'] ?? []);
$mode = $auditMessage->mode;
$resource = $auditMessage->resource;
$userAgent = $auditMessage->userAgent;
$ip = $auditMessage->ip;
$user = $auditMessage->user;
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
$actorUserId = $impersonatorUserId ?: $user->getId();
@@ -126,14 +127,14 @@ class Audits extends Action
];
}
if (isset($this->logs[$project->getSequence()])) {
$this->logs[$project->getSequence()]['logs'][] = $eventData;
if (isset($this->logs[$auditMessage->project->getSequence()])) {
$this->logs[$auditMessage->project->getSequence()]['logs'][] = $eventData;
} else {
$this->logs[$project->getSequence()] = [
$this->logs[$auditMessage->project->getSequence()] = [
'project' => new Document([
'$id' => $project->getId(),
'$sequence' => $project->getSequence(),
'database' => $project->getAttribute('database'),
'$id' => $auditMessage->project->getId(),
'$sequence' => $auditMessage->project->getSequence(),
'database' => $auditMessage->project->getAttribute('database'),
]),
'logs' => [$eventData]
];
+23 -18
View File
@@ -3,10 +3,10 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
@@ -55,7 +55,7 @@ class Certificates extends Action
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('log')
->inject('certificates')
->inject('plan')
@@ -71,7 +71,7 @@ class Certificates extends Action
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $queueForCertificates
* @param Certificate $publisherForCertificates
* @param Log $log
* @param CertificatesAdapter $certificates
* @param array $plan
@@ -88,7 +88,7 @@ class Certificates extends Action
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Log $log,
CertificatesAdapter $certificates,
array $plan,
@@ -100,21 +100,22 @@ class Certificates extends Action
throw new Exception('Missing payload');
}
$document = new Document($payload['domain'] ?? []);
$certificateMessage = \Appwrite\Event\Message\Certificate::fromArray($payload);
$document = $certificateMessage->domain;
$domain = new Domain($document->getAttribute('domain', ''));
$domainType = $document->getAttribute('domainType');
$skipRenewCheck = $payload['skipRenewCheck'] ?? false;
$validationDomain = $payload['validationDomain'] ?? null;
$action = $payload['action'] ?? Certificate::ACTION_GENERATION;
$skipRenewCheck = $certificateMessage->skipRenewCheck;
$validationDomain = $certificateMessage->validationDomain;
$action = $certificateMessage->action;
$log->addTag('domain', $domain->get());
switch ($action) {
case Certificate::ACTION_DOMAIN_VERIFICATION:
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $authorization, $validationDomain);
case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION:
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
break;
case Certificate::ACTION_GENERATION:
case \Appwrite\Event\Certificate::ACTION_GENERATION:
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
break;
@@ -130,7 +131,7 @@ class Certificates extends Action
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $queueForCertificates
* @param Certificate $publisherForCertificates
* @param Log $log
* @param ValidatorAuthorization $authorization
* @param string|null $validationDomain
@@ -146,7 +147,7 @@ class Certificates extends Action
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
Certificate $queueForCertificates,
Certificate $publisherForCertificates,
Log $log,
ValidatorAuthorization $authorization,
?string $validationDomain = null
@@ -188,13 +189,17 @@ class Certificates extends Action
// Issue a TLS certificate when domain is verified
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$queueForCertificates
->setDomain(new Document([
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: new Document([
'$id' => $rule->getAttribute('projectId', ''),
'$sequence' => $rule->getAttribute('projectInternalId', 0),
]),
domain: new Document([
'domain' => $rule->getAttribute('domain'),
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
]))
->setAction(Certificate::ACTION_GENERATION)
->trigger();
]),
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
));
Console::success('Certificate generation triggered successfully.');
}
+11 -3
View File
@@ -759,11 +759,19 @@ class Deletes extends Action
$databasesToClean
));
} elseif ($sharedTablesV1) {
/**
* Temporary disabling deletes for internal collections
*/
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$queries[] = Query::orderAsc();
$this->deleteByGroup(
Database::METADATA,
[
Query::orderAsc()
],
$queries,
$dbForProject
);
} elseif ($sharedTablesV2) {
@@ -7,6 +7,8 @@ use Exception;
use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Executions extends Action
{
@@ -39,6 +41,20 @@ class Executions extends Action
throw new Exception('Missing execution');
}
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.executions_worker_upsert');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $executionMessage->project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('resourceType', $execution->getAttribute('resourceType', ''));
Span::current()?->finish();
}
$dbForProject->upsertDocument('executions', $execution);
}
}
@@ -23,6 +23,7 @@ use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Functions extends Action
@@ -115,6 +116,22 @@ class Functions extends Action
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_dequeue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('payloadType', $type);
Span::add('queuePid', $message->getPid());
Span::add('queueName', $message->getQueue());
Span::add('messageTimestamp', (string) $message->getTimestamp());
Span::current()?->finish();
}
}
if (!empty($events)) {
$limit = 100;
$sum = 100;
@@ -304,6 +321,20 @@ class Functions extends Action
'duration' => 0.0,
]);
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('trigger', $trigger);
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -522,6 +553,18 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_executor');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $executionId);
Span::add('deploymentId', $deployment->getId());
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
@@ -594,6 +637,19 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
+24 -14
View File
@@ -19,8 +19,7 @@ abstract class Promise
return;
}
$resolve = function ($value) {
$this->setResult($value);
$this->setState(self::STATE_FULFILLED);
$this->setState($this->setResult($value));
};
$reject = function ($value) {
$this->setResult($value);
@@ -106,6 +105,11 @@ abstract class Promise
}
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
if (!\is_callable($callable)) {
if ($this->isRejected()) {
$reject($this->result);
return;
}
$resolve($this->result);
return;
}
@@ -126,30 +130,36 @@ abstract class Promise
abstract public static function all(iterable $promises): self;
/**
* Set resolved result
* Set the resolved result, adopting nested promises while preserving
* whether the adopted promise fulfilled or rejected.
*
* @param mixed $value
* @return void
* @return int
*/
protected function setResult(mixed $value): void
protected function setResult(mixed $value): int
{
if (!\is_callable([$value, 'then'])) {
$this->result = $value;
return;
return self::STATE_FULFILLED;
}
$resolved = false;
$state = self::STATE_PENDING;
$callable = function ($value) use (&$resolved) {
$this->setResult($value);
$resolved = true;
};
$value->then(
function ($value) use (&$state) {
$state = $this->setResult($value);
},
function ($value) use (&$state) {
$this->result = $value;
$state = self::STATE_REJECTED;
}
);
$value->then($callable, $callable);
while (!$resolved) {
while ($state === self::STATE_PENDING) {
usleep(25000);
}
return $state;
}
/**
+42
View File
@@ -78,6 +78,18 @@ abstract class Format
],
];
private const array REQUEST_PARAMETER_OVERRIDES = [
[
'namespace' => 'project',
'methods' => [
'createWebPlatform',
'updateWebPlatform',
],
'parameter' => 'hostname',
'required' => true,
],
];
protected array $enumBlacklist = [];
public function __construct(Container $container, array $services, array $routes, array $models, array $keys, int $authCount, string $platform)
@@ -774,8 +786,38 @@ abstract class Format
return $values;
}
protected function getRequestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array
{
$config = [
'required' => !$optional,
'nullable' => $nullable,
];
foreach (self::REQUEST_PARAMETER_OVERRIDES as $override) {
if (
$override['namespace'] !== $service
|| !\in_array($method, $override['methods'], true)
|| $override['parameter'] !== $param
) {
continue;
}
$config['required'] = $override['required'] ?? $config['required'];
$config['nullable'] = $override['nullable'] ?? $config['nullable'];
break;
}
$config['emitDefault'] = !$config['required'] && !\is_null($default);
return $config;
}
public function getResponseEnumName(string $model, string $param): ?string
{
if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') {
return 'PlatformType';
}
if ($param !== 'status') {
return null;
}
@@ -381,14 +381,23 @@ class OpenAPI3 extends Format
*/
$validator = $this->getValidator($param);
$isNullable = $validator instanceof Nullable;
$parameter = $this->getRequestParameterConfig(
$sdk->getNamespace() ?? '',
$methodName,
$name,
$param['optional'],
$isNullable,
$param['default'],
);
$node = [
'name' => $name,
'description' => $param['description'],
'required' => !$param['optional'],
'required' => $parameter['required'],
];
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
@@ -735,7 +744,7 @@ class OpenAPI3 extends Format
break;
}
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
if ($parameter['emitDefault']) { // Param has default value
$node['schema']['default'] = $param['default'];
}
@@ -746,7 +755,7 @@ class OpenAPI3 extends Format
$node['in'] = 'query';
$temp['parameters'][] = $node;
} else { // Param is in payload
if (!$param['optional']) {
if ($node['required']) {
$bodyRequired[] = $name;
}
@@ -783,7 +792,7 @@ class OpenAPI3 extends Format
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
if ($parameter['nullable']) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
}
}
@@ -383,14 +383,23 @@ class Swagger2 extends Format
/** @var Validator $validator */
$validator = $this->getValidator($param);
$isNullable = $validator instanceof Nullable;
$parameter = $this->getRequestParameterConfig(
$sdk->getNamespace() ?? '',
$methodName,
$name,
$param['optional'],
$isNullable,
$param['default'],
);
$node = [
'name' => $name,
'description' => $param['description'],
'required' => !$param['optional'],
'required' => $parameter['required'],
];
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
@@ -711,7 +720,7 @@ class Swagger2 extends Format
break;
}
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
if ($parameter['emitDefault']) { // Param has default value
$node['default'] = $param['default'];
}
@@ -729,7 +738,7 @@ class Swagger2 extends Format
continue;
}
if (!$param['optional']) {
if ($node['required']) {
$bodyRequired[] = $name;
}
@@ -755,7 +764,7 @@ class Swagger2 extends Format
$body['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
if ($parameter['nullable']) {
$body['schema']['properties'][$name]['x-nullable'] = true;
}
@@ -5,6 +5,28 @@ namespace Appwrite\Utopia\Database\Validator\Queries;
class Webhooks extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'url',
'authUsername',
'tls',
'events',
'enabled',
'logs',
'attempts',
];
/**
* Map API attribute names to DB column names.
*/
private const ATTRIBUTE_ALIASES = [
'tls' => 'security',
'authUsername' => 'httpUser',
];
/**
* DB column names used for schema validation.
*/
private const DB_ATTRIBUTES = [
'name',
'url',
'httpUser',
@@ -21,6 +43,26 @@ class Webhooks extends Base
*/
public function __construct()
{
parent::__construct('webhooks', self::ALLOWED_ATTRIBUTES);
parent::__construct('webhooks', self::DB_ATTRIBUTES);
}
/**
* Convert API attribute names to DB column names in query strings before validation.
*/
public function isValid($value): bool
{
if (\is_array($value)) {
foreach ($value as &$queryString) {
if (!\is_string($queryString)) {
continue;
}
foreach (self::ATTRIBUTE_ALIASES as $alias => $dbName) {
$queryString = \str_replace('"' . $alias . '"', '"' . $dbName . '"', $queryString);
}
}
unset($queryString);
}
return parent::isValid($value);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
class V22 extends Filter
{
// Convert 1.9.0 params to 1.9.1
protected function parseUpdateProtocolStatus(array $content): array
{
if (isset($content['api'])) {
$content['protocolId'] = $content['api'];
unset($content['api']);
}
if (isset($content['status'])) {
$content['enabled'] = $content['status'];
unset($content['status']);
}
if (($content['protocolId'] ?? '') === 'realtime') {
$content['protocolId'] = 'websocket';
}
return $content;
}
protected function parseUpdateServiceStatus(array $content): array
{
if (isset($content['service'])) {
$content['serviceId'] = $content['service'];
unset($content['service']);
}
if (isset($content['status'])) {
$content['enabled'] = $content['status'];
unset($content['status']);
}
return $content;
}
protected function parseKeyScopes(array $content): array
{
if (!\is_array($content['scopes'] ?? null)) {
$content['scopes'] = [];
}
return $content;
}
protected function parseWebhook(array $content): array
{
if (isset($content['security'])) {
$content['tls'] = $content['security'];
unset($content['security']);
}
if (isset($content['httpUser'])) {
$content['authUsername'] = $content['httpUser'];
unset($content['httpUser']);
}
if (isset($content['httpPass'])) {
$content['authPassword'] = $content['httpPass'];
unset($content['httpPass']);
}
return $content;
}
public function parse(array $content, string $model): array
{
switch ($model) {
case 'project.updateServiceStatus':
$content = $this->parseUpdateServiceStatus($content);
break;
case 'project.updateProtocolStatus':
$content = $this->parseUpdateProtocolStatus($content);
break;
case 'project.createKey':
case 'project.updateKey':
$content = $this->parseKeyScopes($content);
break;
case 'webhooks.create':
case 'webhooks.update':
$content = $this->parseWebhook($content);
break;
}
return $content;
}
}
+12
View File
@@ -654,6 +654,8 @@ class Response extends SwooleResponse
throw new \Exception('Response body is not a valid JSON object.');
}
$this->payload = \is_array($data) ? $data : (array) $data;
$this
->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8)
->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
@@ -667,6 +669,16 @@ class Response extends SwooleResponse
return $this->payload;
}
/**
* Set the sent flag on the response. Pass false to allow reuse
* (e.g. batched GraphQL queries), true to prevent further writes.
*/
public function setSent(bool $sent): static
{
$this->sent = $sent;
return $this;
}
/**
* Function to add a response filter, the order of filters are first in - first out.
*
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
// Convert 1.9.1 Data format to 1.9.0 format
class V22 extends Filter
{
public function parse(array $content, string $model): array
{
return match ($model) {
Response::MODEL_PROJECT => $this->parseProject($content),
Response::MODEL_WEBHOOK => $this->parseWebhook($content),
Response::MODEL_WEBHOOK_LIST => $this->handleList($content, 'webhooks', fn ($item) => $this->parseWebhook($item)),
default => $content,
};
}
private function parseProject(array $content): array
{
foreach (['protocolStatusForRest', 'protocolStatusForGraphql', 'protocolStatusForWebsocket'] as $field) {
unset($content[$field]);
}
return $content;
}
private function parseWebhook(array $content): array
{
$content['security'] = $content['tls'] ?? true;
unset($content['tls']);
$content['httpUser'] = $content['authUsername'] ?? '';
unset($content['authUsername']);
$content['httpPass'] = $content['authPassword'] ?? '';
unset($content['authPassword']);
$content['signatureKey'] = $content['secret'] ?? '';
unset($content['secret']);
return $content;
}
}
@@ -344,6 +344,22 @@ class Project extends Model
])
;
}
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$name = $api['name'] ?? '';
$key = $api['key'] ?? '';
$this
->addRule('protocolStatusFor' . ucfirst($key), [
'type' => self::TYPE_BOOLEAN,
'description' => $name . ' protocol status',
'example' => true,
'default' => true,
])
;
}
}
/**
@@ -375,6 +391,7 @@ class Project extends Model
{
$this->expandSmtpFields($document);
$this->expandServiceFields($document);
$this->expandApiFields($document);
$this->expandAuthFields($document);
$this->expandOAuthProviders($document);
@@ -419,6 +436,22 @@ class Project extends Model
}
}
private function expandApiFields(Document $document): void
{
if (!$document->isSet('apis')) {
return;
}
$values = $document->getAttribute('apis', []);
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$key = $api['key'] ?? '';
$value = $values[$key] ?? true;
$document->setAttribute('protocolStatusFor' . ucfirst($key), $value);
}
}
private function expandAuthFields(Document $document): void
{
if (!$document->isSet('auths')) {
+24 -6
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class Webhook extends Model
{
@@ -50,27 +51,27 @@ class Webhook extends Model
],
'array' => true,
])
->addRule('security', [
->addRule('tls', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Indicated if SSL / TLS Certificate verification is enabled.',
'description' => 'Indicates if SSL / TLS certificate verification is enabled.',
'default' => true,
'example' => true,
])
->addRule('httpUser', [
->addRule('authUsername', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication username.',
'default' => '',
'example' => 'username',
])
->addRule('httpPass', [
->addRule('authPassword', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication password.',
'default' => '',
'example' => 'password',
])
->addRule('signatureKey', [
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'Signature key which can be used to validated incoming',
'description' => 'Signature key which can be used to validate incoming webhook payloads. Only returned on creation and secret rotation.',
'default' => '',
'example' => 'ad3d581ca230e2b7059c545e5a',
])
@@ -94,6 +95,23 @@ class Webhook extends Model
]);
}
public function filter(Document $document): Document
{
$document->setAttribute('tls', $document->getAttribute('security'));
$document->removeAttribute('security');
$document->setAttribute('authUsername', $document->getAttribute('httpUser'));
$document->removeAttribute('httpUser');
$document->setAttribute('authPassword', $document->getAttribute('httpPass'));
$document->removeAttribute('httpPass');
$document->setAttribute('secret', $document->getAttribute('signatureKey'));
$document->removeAttribute('signatureKey');
return $document;
}
/**
* Get Name
*
+1 -1
View File
@@ -22,7 +22,7 @@ class Comment
'Every Git commit and branch gets its own deployment URL automatically',
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
'HTTPS and SSL certificates are handled automatically for all your Sites',
'Functions can run for up to 15 minutes before timing out',
'Function builds can take up to 45 minutes before timing out',
'Schedule functions to run as often as every minute with cron expressions',
'Environment variables can be scoped per function or shared across your project',
'Function scopes give you fine-grained control over API permissions',
+2 -2
View File
@@ -216,7 +216,7 @@ trait ProjectCustom
'users.*'
],
'url' => 'http://request-catcher-webhook:5000/',
'security' => false,
'tls' => false,
]);
$this->assertEquals(201, $webhook['headers']['status-code']);
@@ -243,7 +243,7 @@ trait ProjectCustom
'apiKey' => $key['body']['secret'],
'devKey' => $devKey['body']['secret'],
'webhookId' => $webhook['body']['$id'],
'signatureKey' => $webhook['body']['signatureKey'],
'signatureKey' => $webhook['body']['secret'],
];
}
@@ -4160,178 +4160,4 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
/**
* Test that a new email/password session is immediately usable even when
* a concurrent request re-populates the user cache between the cache purge
* and session creation.
*
* Regression test for: purging the user cache BEFORE persisting the session
* allows a concurrent request (from a different Swoole worker) to re-cache
* a stale user document that lacks the new session, causing sessionVerify
* to fail with 401 on subsequent requests using the new session.
*/
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
{
$projectId = $this->getProject()['$id'];
$endpoint = $this->client->getEndpoint();
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
$password = 'password123!';
// Create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Race Test User',
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Login to get session A
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $responseA['headers']['status-code']);
$sessionA = $responseA['cookies']['a_session_' . $projectId];
// Verify session A works
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
]);
$this->assertEquals(200, $verifyA['headers']['status-code']);
/**
* Race condition scenario:
* 1. Start login B via curl_multi (non-blocking)
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
* 3. THEN add GET requests to curl_multi - these hit different workers and
* re-cache a stale user document (without session B) during the window
* between purgeCachedDocument and createDocument
* 4. After all complete, verify session B is usable
*/
for ($attempt = 0; $attempt < 5; $attempt++) {
$loginCookies = [];
$multi = curl_multi_init();
// Start login B first (alone)
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
curl_setopt_array($loginHandle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
],
CURLOPT_POSTFIELDS => \json_encode([
'email' => $email,
'password' => $password,
]),
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
if (\stripos($header, 'set-cookie:') === 0) {
$cookiePart = \trim(\substr($header, 11));
$eqPos = \strpos($cookiePart, '=');
if ($eqPos !== false) {
$name = \substr($cookiePart, 0, $eqPos);
$rest = \substr($cookiePart, $eqPos + 1);
$semiPos = \strpos($rest, ';');
$loginCookies[$name] = $semiPos !== false
? \substr($rest, 0, $semiPos)
: $rest;
}
}
return \strlen($header);
},
]);
curl_multi_add_handle($multi, $loginHandle);
// Drive the login transfer forward and wait for the server to start
// processing the login (past hash verification + cache purge).
$deadline = \microtime(true) + 0.15; // 150ms
do {
curl_multi_exec($multi, $active);
curl_multi_select($multi, 0.005);
} while (\microtime(true) < $deadline && $active);
// NOW add GET requests - they arrive after the cache purge
// but before session creation (which is delayed by the usleep or I/O).
$getHandles = [];
for ($i = 0; $i < 10; $i++) {
$gh = curl_init("{$endpoint}/account");
curl_setopt_array($gh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
"cookie: a_session_{$projectId}={$sessionA}",
],
]);
curl_multi_add_handle($multi, $gh);
$getHandles[] = $gh;
}
// Drive all to completion
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi, 0.05);
}
} while ($active && $status === CURLM_OK);
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($multi, $loginHandle);
curl_close($loginHandle);
foreach ($getHandles as $gh) {
curl_multi_remove_handle($multi, $gh);
curl_close($gh);
}
curl_multi_close($multi);
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
// THE CRITICAL CHECK: verify session B is usable immediately
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
$this->assertEquals(
200,
$verifyB['headers']['status-code'],
'Session B must be immediately usable after login. '
. 'A 401 here means a stale user cache (without the new session) was served. '
. 'The fix is to create the session document BEFORE purging the user cache.'
);
// Clean up session B for next iteration
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
}
}
}

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