mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.9.x' into fix-large-file-download
This commit is contained in:
@@ -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
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -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
@@ -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',
|
||||
],
|
||||
];
|
||||
@@ -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'],
|
||||
],
|
||||
|
||||
@@ -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' => ''
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -1288,6 +1288,7 @@ services:
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: on-failure:3
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 === [];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -93,6 +93,7 @@ abstract class Migration
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+24
-7
@@ -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;
|
||||
|
||||
+18
-5
@@ -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;
|
||||
|
||||
+6
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
-59
@@ -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(...));
|
||||
}
|
||||
}
|
||||
-59
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
];
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user