Compare commits

...
71 Commits
Author SHA1 Message Date
ArnabChatterjee20kandGitHub ebc3febc38 Merge branch '1.9.x' into docsdb-fixes 2026-04-15 09:42:05 +05:30
Chirag AggarwalandGitHub 8fcba8b5c9 Merge pull request #11893 from appwrite/codex/disable-graphql-functional 2026-04-14 19:19:14 +05:30
Jake BarnbyandGitHub 6c8b34c230 Merge pull request #11885 from appwrite/fix-gql-route-reset
(fix): reset route to avoid clobbering otel
2026-04-15 01:26:02 +12:00
Shimon NewmanandGitHub 377ade3c44 Merge pull request #11892 from appwrite/schedule-functions-debug-2
Schedule functions debug
2026-04-14 15:32:29 +03:00
Shimon NewmanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
71533aaaf3 Update app/cli.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 15:17:37 +03:00
shimon ccbe7e5d3f Merge branch '1.9.x' of github.com:appwrite/appwrite into schedule-functions-debug-2 2026-04-14 15:11:56 +03:00
Chirag AggarwalandGitHub bea7739d7e Merge branch '1.9.x' into codex/disable-graphql-functional 2026-04-14 17:21:55 +05:30
Jake BarnbyandGitHub 76320d652a Merge pull request #11887 from appwrite/fix/composer-audit-graphql-php
Upgrade graphql-php to v15.31.5
2026-04-14 23:46:45 +12:00
Chirag Aggarwal 6d5968e2ea ci: disable functional GraphQL e2e 2026-04-14 16:51:46 +05:30
shimon 70c380fa36 feat: add tracing support for execution events in log and worker classes 2026-04-14 13:36:29 +03:00
shimon 512a7ae2bd feat: enhance function scheduling with tracing support 2026-04-14 13:36:19 +03:00
Chirag AggarwalandGitHub 49d3cd980f Merge branch '1.9.x' into fix/composer-audit-graphql-php 2026-04-14 15:07:22 +05:30
Luke B. SilverandGitHub 39a396810a Merge pull request #11881 from appwrite/feat/45-min-build-timeout
feat: increase default build timeout to 45 minutes
2026-04-14 09:28:29 +01:00
Jake BarnbyandGitHub 1d307178bb Merge pull request #11691 from appwrite/delete-project-shared-table-v1
Safe delete shared tables v1
2026-04-14 18:00:00 +12:00
fogelito c3ed2ff6ce Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-14 08:05:44 +03:00
Chirag Aggarwal efadf17bfe Fix GraphQL 15 static analysis 2026-04-14 10:26:59 +05:30
Chirag Aggarwal bcfec8d5de Align graphql version pinning style 2026-04-14 09:35:25 +05:30
Chirag Aggarwal 4b2e22d9da Fix graphql-php audit vulnerability 2026-04-14 09:27:14 +05:30
ArnabChatterjee20k 45e22e2243 Merge remote-tracking branch 'origin/1.9.x' into docsdb-fixes 2026-04-13 22:44:07 +05:30
Matej BačoandGitHub 0a21f3b139 Merge pull request #11884 from appwrite/fix-webhook-api-security
Fix: Webhook API secret rotation and security
2026-04-13 15:52:55 +02:00
Jake Barnby cc8eb62c83 (chore): rename 2026-04-14 01:15:06 +12:00
Luke B. SilverandGitHub b1ad6ea87c Merge pull request #11883 from appwrite/fix/large-execution-payload
fix: trim execution queue payload to project ID only
2026-04-13 14:13:43 +01:00
Jake Barnby d52d6c0bf0 (fix): reset route to avoid clobbering otel 2026-04-14 01:13:32 +12:00
Matej Bačo db406b0a27 Fix tests 2026-04-13 15:08:20 +02:00
Matej Bačo 2585518e33 Fix bugs 2026-04-13 15:08:13 +02:00
Matej Bačo c9fceb870c Fix folder structure 2026-04-13 14:57:24 +02:00
Matej Bačo 9f1ec356d1 Formatting fix 2026-04-13 14:53:25 +02:00
Matej Bačo 28d285d5c5 Improved tests for webhook edge cases 2026-04-13 14:53:06 +02:00
loks0nandClaude Sonnet 4.6 a941d8b855 fix: trim execution queue payload to project ID only
The v1-executions queue was serialising the full project document
(OAuth providers, webhooks, keys, auths config, permissions, etc.)
into every message. The worker DI system already re-fetches the
complete project from the platform database using only the project ID,
so the full document was wasted bytes.

Override trimPayload() in the Execution event class to include only
$id in the project stub, reducing message size significantly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:52:09 +01:00
Matej Bačo 3263133e5f Implement secure webhook interfaces 2026-04-13 14:50:06 +02:00
fogelito a3ba66c90a Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1
# Conflicts:
#	composer.lock
2026-04-13 13:29:03 +03:00
ArnabChatterjee20kandGitHub c24d724000 Merge pull request #11749 from appwrite/db-workers-memory
added reset in db worker for queue for realtime
2026-04-13 15:47:54 +05:30
Luke B. SilverGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
df5ccc10ad Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-13 10:48:20 +01:00
loks0nandClaude Sonnet 4.6 2807d6cd9a feat: increase default build timeout to 45 minutes
Raises _APP_COMPUTE_BUILD_TIMEOUT default from 900s (15 min) to
2700s (45 min) to support longer-running builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 09:57:27 +01:00
ArnabChatterjee20kandGitHub 20e2f2284f Merge branch '1.9.x' into db-workers-memory 2026-04-13 13:43:14 +05:30
Matej BačoandGitHub 7a0d69c826 Merge pull request #11839 from appwrite/feat-services-protocols-apis
Feat: services protocols public apis
2026-04-13 09:38:17 +02:00
Matej Bačo feedec80f2 Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 09:17:09 +02:00
Chirag AggarwalandGitHub 584acafb1d Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 10:45:42 +05:30
Matej Bačo 27fc8058b9 Fix failing tests 2026-04-11 14:19:05 +02:00
Matej Bačo c5bd8c712f Upgrade libs 2026-04-11 11:31:35 +02:00
Matej Bačo a1267b1bff Backwards compatibiltiy tests 2026-04-11 11:16:43 +02:00
Matej Bačo 18d17ea945 Webhook endpoints quality improvements 2026-04-11 11:00:56 +02:00
Matej Bačo fabd9559c4 Tests for backwards compatibility 2026-04-11 10:22:03 +02:00
Matej Bačo ec637d4417 Mark key scopes required 2026-04-11 10:19:14 +02:00
ArnabChatterjee20k 0cce480592 typo 2026-04-10 17:23:43 +05:30
ArnabChatterjee20k b622b092a8 updated 2026-04-10 17:22:22 +05:30
ArnabChatterjee20k 96fe989f6d update composer dependencies and remove obsolete log classes 2026-04-10 17:15:31 +05:30
ArnabChatterjee20k d13dbae0fe added allowance of empty payload for documentsdb 2026-04-10 13:59:45 +05:30
Matej Bačo d69726487e PR review fixes 2026-04-09 16:58:42 +02:00
Matej Bačo 5fccb8cc28 Improve tests 2026-04-09 16:57:44 +02:00
Matej Bačo 21a0d60c98 Fix tests 2026-04-09 16:13:54 +02:00
Matej Bačo 4eb8534294 Fix tests 2026-04-09 16:08:11 +02:00
Matej Bačo c95f905bce New services and protocols tests 2026-04-09 15:58:28 +02:00
Matej Bačo 0293da1e22 Improve test for backwards compatibility 2026-04-09 15:54:00 +02:00
Matej Bačo a4a0c4175d Implement new endpoints in /v1/project for services and protocols 2026-04-09 15:45:06 +02:00
Matej Bačo d3c73fbb49 Add endpoints to control protocols and services 2026-04-09 15:34:50 +02:00
Matej Bačo 8818187740 Introduce req&res filters for 1.9.1 2026-04-09 15:21:58 +02:00
Matej Bačo 75324b24fc Improve skill 2026-04-09 15:21:24 +02:00
Matej Bačo d6d118f4ab Bump version to 1.9.1 2026-04-09 15:19:58 +02:00
Matej Bačo e998739998 Add agent skill to increase patch version 2026-04-09 15:18:26 +02:00
fogelito 31728c9b72 Update lock 2026-04-06 09:29:58 +03:00
fogelito 9d0fc9e5b1 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-06 09:19:38 +03:00
fogelito a332eb5f32 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-05 11:42:25 +03:00
fogelito 775d21e5ee lock 2026-04-05 10:08:29 +03:00
ArnabChatterjee20k cf99269bc5 Add reset method to Realtime class for clearing event state
This new method resets the event state for long-lived worker processes by clearing subscribers, context, and user-related fields, ensuring no stale state affects subsequent triggers.
2026-04-01 18:08:45 +05:30
ArnabChatterjee20k 2a184288f4 removed throw 2026-04-01 18:05:56 +05:30
ArnabChatterjee20k 9237f387fc added reset in db worker for queue for realtime 2026-04-01 17:59:28 +05:30
fogelito e42a26fdad comment 2026-03-31 19:20:13 +03:00
fogelito 2c4770d29b Database 5.3.19 2026-03-31 19:18:32 +03:00
fogelito 2a9e423cb1 Temporary disabling deletes from internal collections 2026-03-31 12:54:33 +03:00
fogelito 14869cc6d6 Safe delete shared tables v1 2026-03-31 09:59:53 +03:00
70 changed files with 2564 additions and 718 deletions
@@ -0,0 +1,29 @@
# Patch Release Checklist for Appwrite
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
## Checklist
### Bump console image
Update the console Docker image tag in both files:
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
### Bump Appwrite version
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
### Update CHANGES.md
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
## Final review
- [ ] Ask user to review changes before commiting
- [ ] Ask user to update `CHANGES.md` with PRs
- [ ] Ask user to generate specs, if needed
- [ ] Ask user to add request and response filters, if needed
+1 -1
View File
@@ -512,7 +512,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
+4
View File
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
## Cross-repo context
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
@@ -9,8 +9,8 @@ return [
'key' => 'graphql',
'name' => 'GraphQL',
],
'realtime' => [
'key' => 'realtime',
'name' => 'Realtime',
'websocket' => [
'key' => 'websocket',
'name' => 'Websocket',
],
];
+4 -4
View File
@@ -137,7 +137,7 @@ return [
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -193,7 +193,7 @@ return [
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -235,7 +235,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/proxy',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
@@ -291,7 +291,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => false,
'optional' => true,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
+4 -4
View File
@@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
+1 -1
View File
@@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+4 -186
View File
@@ -71,202 +71,20 @@ Http::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatus',
description: '/docs/references/projects/update-service-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$services = $project->getAttribute('services', []);
$services[$service] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatusAll',
description: '/docs/references/projects/update-service-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
$services[$service] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/api')
->desc('Update API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$apis = $project->getAttribute('apis', []);
$apis[$api] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/api/all')
->desc('Update all API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allApis = array_keys(Config::getParam('apis'));
$apis = [];
foreach ($allApis as $api) {
$apis[$api] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/oauth2')
+9
View File
@@ -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());
}
@@ -1419,6 +1427,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
+10 -9
View File
@@ -425,7 +425,7 @@ Http::init()
}
if (! empty($method)) {
$namespace = $method->getNamespace();
$namespace = \strtolower($method->getNamespace());
if (
array_key_exists($namespace, $project->getAttribute('services', []))
@@ -436,6 +436,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))) {
@@ -511,14 +520,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
*/
+1 -1
View File
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
+2 -2
View File
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.9.0';
const APP_CACHE_BUSTER = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
+4 -2
View File
@@ -653,9 +653,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$user = $connectionContainer->get('user'); /** @var User $user */
$logUser = $user;
$apis = $project->getAttribute('apis', []);
// Websocket is what to check, but realtime is checked too for backwards compatibility
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
+1 -1
View File
@@ -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
+40 -26
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "f6a87c1012b316e614258f8f57a28e48",
"packages": [
{
"name": "adhocore/jwt",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.20",
"version": "5.3.21",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"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.20"
"source": "https://github.com/utopia-php/database/tree/5.3.21"
},
"time": "2026-04-10T08:27:41+00:00"
"time": "2026-04-10T12:38:57+00:00"
},
{
"name": "utopia-php/detector",
@@ -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,15 +5444,19 @@
],
"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": [
@@ -8449,5 +8463,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+22 -2
View File
@@ -7,6 +7,8 @@ use Appwrite\Event\Message\Execution as ExecutionMessage;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -30,9 +32,27 @@ class Log extends Listener
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
{
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
project: new Document($event->project),
execution: new Document($event->execution),
project: $project,
execution: $execution,
));
}
}
+19
View File
@@ -53,4 +53,23 @@ class Execution extends Event
'execution' => $this->execution,
];
}
/**
* Trim payload for the execution event.
* Only the project ID is needed — the worker DI fetches the full project from the platform database.
*
* @return array
*/
protected function trimPayload(): array
{
$trimmed = [];
if ($this->project) {
$trimmed['project'] = new Document([
'$id' => $this->project->getId(),
]);
}
return $trimmed;
}
}
+20
View File
@@ -61,6 +61,26 @@ class Realtime extends Event
return $this->subscribers;
}
/**
* Reset the event state for long-lived worker processes.
*
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
* to prevent stale state from affecting subsequent triggers.
*/
public function reset(): self
{
parent::reset();
$this->subscribers = [];
$this->context = [];
$this->project = null;
$this->user = null;
$this->userId = null;
return $this;
}
/**
* Execute Event.
*
+2 -2
View File
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
/**
* Create a new promise that resolves when all passed in promises resolve.
*
* @param array $promisesOrValues
* @param iterable $promisesOrValues
* @return GQLPromise
*/
abstract public function all(array $promisesOrValues): GQLPromise;
abstract public function all(iterable $promisesOrValues): GQLPromise;
}
@@ -35,8 +35,12 @@ class Swoole extends Adapter
return new GQLPromise($promise, $this);
}
public function all(array $promisesOrValues): GQLPromise
public function all(iterable $promisesOrValues): GQLPromise
{
if ($promisesOrValues instanceof \Traversable) {
$promisesOrValues = \iterator_to_array($promisesOrValues);
}
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
}
}
+6
View File
@@ -264,6 +264,8 @@ class Resolvers
$response->setContentType(Response::CONTENT_TYPE_NULL);
$response->clearSent();
$original = $utopia->getRoute();
try {
$route = $utopia->match($request, fresh: true);
@@ -274,6 +276,10 @@ class Resolvers
}
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
}
$payload = $response->getPayload();
+8 -3
View File
@@ -3,12 +3,13 @@
namespace Appwrite\GraphQL\Types;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Assoc extends Json
{
public $name = 'Assoc';
public $description = 'The `Assoc` scalar type represents associative array values.';
public string $name = 'Assoc';
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
public function serialize($value)
{
@@ -30,6 +31,10 @@ class Assoc extends Json
public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return \json_decode($valueNode->value, true);
if ($valueNode instanceof StringValueNode) {
return \json_decode($valueNode->value, true);
}
return parent::parseLiteral($valueNode, $variables);
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
class InputFile extends ScalarType
{
public $name = 'InputFile';
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
public string $name = 'InputFile';
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
public function serialize($value)
+2 -2
View File
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Json extends ScalarType
{
public $name = 'Json';
public $description = 'The `JSON` scalar type represents JSON values as specified by
public string $name = 'Json';
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
public function serialize($value)
+1
View File
@@ -93,6 +93,7 @@ abstract class Migration
'1.8.0' => 'V23',
'1.8.1' => 'V23',
'1.9.0' => 'V24',
'1.9.1' => 'V24',
];
/**
@@ -50,6 +50,11 @@ class Create extends Action
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return false;
}
public function __construct()
{
$this
@@ -139,30 +144,42 @@ class Create extends Action
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
$hasData = !empty($data);
$hasDocuments = !empty($documents);
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// When empty documents are supported, an empty payload should still be treated as single create.
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
$data = [];
$hasData = true;
}
if ($hasData && $hasDocuments) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
}
if (!empty($data) && empty($documentId)) {
if ($hasData && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
if ($hasDocuments && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
@@ -170,13 +187,13 @@ class Create extends Action
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
if ($hasDocuments && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
$isBulk = $hasDocuments;
if ($hasData) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listDocumentsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/documentsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listDocumentsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/documentsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listVectorsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/vectorsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,57 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listVectorsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/vectorsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
$service->addAction(UpdateTable::getName(), new UpdateTable());
$service->addAction(DeleteTable::getName(), new DeleteTable());
$service->addAction(ListTables::getName(), new ListTables());
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
}
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
$service->addAction(DeleteRow::getName(), new DeleteRow());
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
@@ -69,7 +67,6 @@ class VectorsDB extends Base
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
$service->addAction(ListCollections::getName(), new ListCollections());
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
}
@@ -92,7 +89,6 @@ class VectorsDB extends Base
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
}
private function registerTransactionActions(Service $service): void
@@ -650,26 +650,30 @@ class Databases extends Action
Document|null $attribute = null,
Document|null $index = null,
): void {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
try {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
}
$queueForRealtime->trigger();
} finally {
$queueForRealtime->reset();
}
$queueForRealtime->trigger();
}
}
@@ -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,
]);
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
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.[protocol].update')
->label('audits.event', 'project.protocols.[protocol].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')
->callback($this->action(...));
}
public function action(
string $protocolId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$protocols = $project->getAttribute('apis', []);
$protocols[$protocolId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'apis' => $protocols,
])));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
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.[service].update')
->label('audits.event', 'project.services.[service].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')
->callback($this->action(...));
}
public function action(
string $serviceId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$services = $project->getAttribute('services', []);
$services[$serviceId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'services' => $services,
])));
$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());
@@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -65,9 +66,10 @@ class Create extends Action
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -85,9 +87,10 @@ class Create extends Action
string $name,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -104,10 +107,10 @@ class Create extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'signatureKey' => \bin2hex(\random_bytes(64)),
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
'enabled' => $enabled,
]);
@@ -72,6 +72,8 @@ class Get extends Action
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
@@ -15,6 +15,8 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
@@ -22,15 +24,15 @@ class Update extends Action
public static function getName()
{
return 'updateWebhookSignature';
return 'updateWebhookSecret';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/webhooks/:webhookId/signature')
->setHttpPath('/v1/webhooks/:webhookId/secret')
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature')
->desc('Update webhook signature key')
->desc('Update webhook secret key')
->groups(['api', 'webhooks'])
->label('scope', 'webhooks.write')
->label('event', 'webhooks.[webhookId].update')
@@ -39,9 +41,9 @@ class Update extends Action
->label('sdk', new Method(
namespace: 'webhooks',
group: null,
name: 'updateSignature',
name: 'updateSecret',
description: <<<EOT
Update the webhook signature key. This endpoint can be used to regenerate the signature key used to sign and validate payload deliveries for a specific webhook.
Update the webhook signing key. This endpoint can be used to regenerate the signing key used to sign and validate payload deliveries for a specific webhook.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
@@ -52,6 +54,7 @@ class Update extends Action
]
))
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -62,6 +65,7 @@ class Update extends Action
public function action(
string $webhookId,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -78,7 +82,7 @@ class Update extends Action
}
$updates = new Document([
'signatureKey' => \bin2hex(\random_bytes(64)),
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
]);
$webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
@@ -63,9 +63,9 @@ class Update extends Action
->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -80,9 +80,9 @@ class Update extends Action
string $url,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -102,9 +102,9 @@ class Update extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'enabled' => $enabled,
]);
@@ -118,6 +118,8 @@ class Update extends Action
$queueForEvents->setParam('webhookId', $webhook->getId());
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -78,6 +78,15 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
foreach ($queries as $query) {
$attribute = $query->getAttribute();
if ($attribute === 'authUsername') {
$query->setAttribute('httpUser');
} elseif ($attribute === 'tls') {
$query->setAttribute('security');
}
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
@@ -111,6 +120,10 @@ class XList extends Action
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
foreach ($webhooks as $webhook) {
$webhook->removeAttribute('signatureKey');
}
$response->dynamic(new Document([
'webhooks' => $webhooks,
'total' => $total,
@@ -6,7 +6,7 @@ use Appwrite\Platform\Modules\Webhooks\Http\Init;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Create as CreateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Delete as DeleteWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Get as GetWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature\Update as UpdateWebhookSignature;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret\Update as UpdateWebhookSecret;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Update as UpdateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\XList as ListWebhooks;
use Utopia\Platform\Service;
@@ -26,6 +26,6 @@ class Http extends Service
$this->addAction(GetWebhook::getName(), new GetWebhook());
$this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
$this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
$this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
$this->addAction(UpdateWebhookSecret::getName(), new UpdateWebhookSecret());
}
}
@@ -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']);
}
+11 -3
View File
@@ -759,11 +759,19 @@ class Deletes extends Action
$databasesToClean
));
} elseif ($sharedTablesV1) {
/**
* Temporary disabling deletes for internal collections
*/
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$queries[] = Query::orderAsc();
$this->deleteByGroup(
Database::METADATA,
[
Query::orderAsc()
],
$queries,
$dbForProject
);
} elseif ($sharedTablesV2) {
@@ -7,6 +7,8 @@ use Exception;
use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Executions extends Action
{
@@ -39,6 +41,20 @@ class Executions extends Action
throw new Exception('Missing execution');
}
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.executions_worker_upsert');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $executionMessage->project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('resourceType', $execution->getAttribute('resourceType', ''));
Span::current()?->finish();
}
$dbForProject->upsertDocument('executions', $execution);
}
}
@@ -23,6 +23,7 @@ use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Functions extends Action
@@ -115,6 +116,22 @@ class Functions extends Action
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_dequeue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('payloadType', $type);
Span::add('queuePid', $message->getPid());
Span::add('queueName', $message->getQueue());
Span::add('messageTimestamp', (string) $message->getTimestamp());
Span::current()?->finish();
}
}
if (!empty($events)) {
$limit = 100;
$sum = 100;
@@ -304,6 +321,20 @@ class Functions extends Action
'duration' => 0.0,
]);
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('trigger', $trigger);
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -522,6 +553,18 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_executor');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $executionId);
Span::add('deploymentId', $deployment->getId());
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
@@ -594,6 +637,19 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -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;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
// Convert 1.9.1 Data format to 1.9.0 format
class V22 extends Filter
{
public function parse(array $content, string $model): array
{
return match ($model) {
Response::MODEL_PROJECT => $this->parseProject($content),
Response::MODEL_WEBHOOK => $this->parseWebhook($content),
Response::MODEL_WEBHOOK_LIST => $this->handleList($content, 'webhooks', fn ($item) => $this->parseWebhook($item)),
default => $content,
};
}
private function parseProject(array $content): array
{
foreach (['protocolStatusForRest', 'protocolStatusForGraphql', 'protocolStatusForWebsocket'] as $field) {
unset($content[$field]);
}
return $content;
}
private function parseWebhook(array $content): array
{
$content['security'] = $content['tls'] ?? true;
unset($content['tls']);
$content['httpUser'] = $content['authUsername'] ?? '';
unset($content['authUsername']);
$content['httpPass'] = $content['authPassword'] ?? '';
unset($content['authPassword']);
$content['signatureKey'] = $content['secret'] ?? '';
unset($content['secret']);
return $content;
}
}
@@ -344,6 +344,22 @@ class Project extends Model
])
;
}
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$name = $api['name'] ?? '';
$key = $api['key'] ?? '';
$this
->addRule('protocolStatusFor' . ucfirst($key), [
'type' => self::TYPE_BOOLEAN,
'description' => $name . ' protocol status',
'example' => true,
'default' => true,
])
;
}
}
/**
@@ -375,6 +391,7 @@ class Project extends Model
{
$this->expandSmtpFields($document);
$this->expandServiceFields($document);
$this->expandApiFields($document);
$this->expandAuthFields($document);
$this->expandOAuthProviders($document);
@@ -419,6 +436,22 @@ class Project extends Model
}
}
private function expandApiFields(Document $document): void
{
if (!$document->isSet('apis')) {
return;
}
$values = $document->getAttribute('apis', []);
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$key = $api['key'] ?? '';
$value = $values[$key] ?? true;
$document->setAttribute('protocolStatusFor' . ucfirst($key), $value);
}
}
private function expandAuthFields(Document $document): void
{
if (!$document->isSet('auths')) {
+24 -6
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class Webhook extends Model
{
@@ -50,27 +51,27 @@ class Webhook extends Model
],
'array' => true,
])
->addRule('security', [
->addRule('tls', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Indicated if SSL / TLS Certificate verification is enabled.',
'description' => 'Indicates if SSL / TLS certificate verification is enabled.',
'default' => true,
'example' => true,
])
->addRule('httpUser', [
->addRule('authUsername', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication username.',
'default' => '',
'example' => 'username',
])
->addRule('httpPass', [
->addRule('authPassword', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication password.',
'default' => '',
'example' => 'password',
])
->addRule('signatureKey', [
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'Signature key which can be used to validated incoming',
'description' => 'Signature key which can be used to validate incoming webhook payloads. Only returned on creation and secret rotation.',
'default' => '',
'example' => 'ad3d581ca230e2b7059c545e5a',
])
@@ -94,6 +95,23 @@ class Webhook extends Model
]);
}
public function filter(Document $document): Document
{
$document->setAttribute('tls', $document->getAttribute('security'));
$document->removeAttribute('security');
$document->setAttribute('authUsername', $document->getAttribute('httpUser'));
$document->removeAttribute('httpUser');
$document->setAttribute('authPassword', $document->getAttribute('httpPass'));
$document->removeAttribute('httpPass');
$document->setAttribute('secret', $document->getAttribute('signatureKey'));
$document->removeAttribute('signatureKey');
return $document;
}
/**
* Get Name
*
+1 -1
View File
@@ -22,7 +22,7 @@ class Comment
'Every Git commit and branch gets its own deployment URL automatically',
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
'HTTPS and SSL certificates are handled automatically for all your Sites',
'Functions can run for up to 15 minutes before timing out',
'Function builds can take up to 45 minutes before timing out',
'Schedule functions to run as often as every minute with cron expressions',
'Environment variables can be scoped per function or shared across your project',
'Function scopes give you fine-grained control over API permissions',
+2 -2
View File
@@ -216,7 +216,7 @@ trait ProjectCustom
'users.*'
],
'url' => 'http://request-catcher-webhook:5000/',
'security' => false,
'tls' => false,
]);
$this->assertEquals(201, $webhook['headers']['status-code']);
@@ -243,7 +243,7 @@ trait ProjectCustom
'apiKey' => $key['body']['secret'],
'devKey' => $devKey['body']['secret'],
'webhookId' => $webhook['body']['$id'],
'signatureKey' => $webhook['body']['signatureKey'],
'signatureKey' => $webhook['body']['secret'],
];
}
@@ -11539,4 +11539,162 @@ trait DatabasesBase
$this->assertEquals('Product B', $rows['body'][$this->getRecordResource()][0]['name']);
$this->assertEquals(139.99, $rows['body'][$this->getRecordResource()][0]['price']);
}
public function testDocumentWithEmptyPayload(): void
{
$data = $this->setupCollection();
$databaseId = $data['databaseId'];
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$this->getRecordIdParam() => ID::unique(),
'data' => [],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
if ($this->getSupportForAttributes()) {
$this->assertEquals(400, $document['headers']['status-code']);
} else {
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
$this->assertArrayNotHasKey('$collection', $document['body']);
$this->assertEquals($databaseId, $document['body']['$databaseId']);
$this->assertTrue(array_key_exists('$sequence', $document['body']));
$this->assertIsString($document['body']['$sequence']);
$documentId = $document['body']['$id'];
$fetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $fetched['headers']['status-code']);
$this->assertEqualsCanonicalizing([
'$id',
'$databaseId',
'$createdAt',
'$updatedAt',
'$permissions',
'$sequence',
$this->getContainerIdResponseKey(),
], \array_keys($fetched['body']));
$this->assertFalse(array_key_exists('$tenant', $fetched['body']));
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()),
[
'data' => [
'status' => 'draft',
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('draft', $updated['body']['status']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $refetched['headers']['status-code']);
$this->assertEquals('draft', $refetched['body']['status']);
}
}
/**
* API keys may set $createdAt / $updatedAt; invalid strings must return 400, not 500.
* Assertions are HTTP status codes only (no error body matching).
*/
public function testInvalidDate(): void
{
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$invalidDatetime = '1dfs:12:55+sdf:00';
$validUpdatedAt = '2024-01-01T00:00:00Z';
$apiKeyHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$documentPayload = [
'title' => 'Captain America',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
],
];
$permissions = [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
];
$invalidCreate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => \array_merge($documentPayload, ['$updatedAt' => $invalidDatetime]),
'permissions' => $permissions,
]);
$this->assertEquals(400, $invalidCreate['headers']['status-code']);
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => $documentPayload,
'permissions' => $permissions,
]);
$this->assertEquals(201, $document['headers']['status-code']);
$documentId = $document['body']['$id'];
$this->assertNotEmpty($documentId);
$invalidPatch = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $invalidDatetime,
],
]
);
$this->assertEquals(400, $invalidPatch['headers']['status-code']);
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $validUpdatedAt,
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders
);
$this->assertEquals(200, $refetched['headers']['status-code']);
}
}
+55 -3
View File
@@ -78,12 +78,12 @@ trait KeysBase
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithNullScopes(): void
public function testCreateKeyWithEmptyScopes(): void
{
$key = $this->createKey(
ID::unique(),
'Null Scopes Key',
null,
'Empty Scopes Key',
[],
);
$this->assertSame(201, $key['headers']['status-code']);
@@ -93,6 +93,58 @@ trait KeysBase
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithNullScopesV22BackwardCompat(): void
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
];
$headers = array_merge($headers, $this->getHeaders());
$key = $this->client->call(Client::METHOD_POST, '/project/keys', $headers, [
'keyId' => ID::unique(),
'name' => 'V22 Compat Key',
'scopes' => null,
]);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame([], $key['body']['scopes']);
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testUpdateKeyWithNullScopesV22BackwardCompat(): void
{
$key = $this->createKey(
ID::unique(),
'V22 Update Compat Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
];
$headers = array_merge($headers, $this->getHeaders());
$updated = $this->client->call(Client::METHOD_PUT, '/project/keys/' . $keyId, $headers, [
'name' => 'V22 Update Compat Key',
'scopes' => null,
]);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame([], $updated['body']['scopes']);
// Cleanup
$this->deleteKey($keyId);
}
public function testCreateKeyWithoutAuthentication(): void
{
$response = $this->createKey(
@@ -0,0 +1,261 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Client;
trait ProtocolsBase
{
protected static array $protocols = ['rest', 'graphql', 'websocket'];
// Success flow
public function testDisableProtocol(): void
{
foreach (self::$protocols as $protocol) {
$response = $this->updateProtocolStatus($protocol, false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(false, $response['body']['protocolStatusFor' . ucfirst($protocol)]);
}
// Cleanup
foreach (self::$protocols as $protocol) {
$this->updateProtocolStatus($protocol, true);
}
}
public function testEnableProtocol(): void
{
// Disable first
foreach (self::$protocols as $protocol) {
$this->updateProtocolStatus($protocol, false);
}
// Re-enable
foreach (self::$protocols as $protocol) {
$response = $this->updateProtocolStatus($protocol, true);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(true, $response['body']['protocolStatusFor' . ucfirst($protocol)]);
}
}
public function testDisableProtocolIdempotent(): void
{
$first = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(false, $first['body']['protocolStatusForRest']);
$second = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(false, $second['body']['protocolStatusForRest']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testEnableProtocolIdempotent(): void
{
$first = $this->updateProtocolStatus('rest', true);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(true, $first['body']['protocolStatusForRest']);
$second = $this->updateProtocolStatus('rest', true);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(true, $second['body']['protocolStatusForRest']);
}
public function testDisabledRestBlocksClientRequest(): void
{
$this->updateProtocolStatus('rest', false);
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testEnabledRestAllowsClientRequest(): void
{
$this->updateProtocolStatus('rest', false);
$this->updateProtocolStatus('rest', true);
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(200, $response['headers']['status-code']);
}
public function testDisabledGraphqlBlocksClientRequest(): void
{
$this->updateProtocolStatus('graphql', false);
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'query' => '{ localeListCountries { total } }',
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testDisableOneProtocolDoesNotAffectOther(): void
{
$this->updateProtocolStatus('graphql', false);
// REST should still work
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(200, $response['headers']['status-code']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testDisabledRestBlocksAllServiceEndpoints(): void
{
$endpoints = [
'account' => '/account',
'teams' => '/teams',
'databases' => '/databases',
'storage' => '/storage/buckets',
'functions' => '/functions',
'sites' => '/sites',
'locale' => '/locale',
'health' => '/health',
'users' => '/users',
'messaging' => '/messaging/providers',
'migrations' => '/migrations',
];
$this->updateProtocolStatus('rest', false);
foreach ($endpoints as $service => $path) {
$response = $this->client->call(Client::METHOD_GET, $path, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code'], 'Disabled REST protocol should block ' . $service . ' endpoint (got ' . $response['headers']['status-code'] . ')');
$this->assertSame('general_api_disabled', $response['body']['type'], 'Disabled REST protocol should return general_api_disabled for ' . $service);
}
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testReenabledRestAllowsAllServiceEndpoints(): void
{
$endpoints = [
'teams' => '/teams',
'databases' => '/databases',
'functions' => '/functions',
'locale' => '/locale',
];
$this->updateProtocolStatus('rest', false);
$this->updateProtocolStatus('rest', true);
foreach ($endpoints as $service => $path) {
$response = $this->client->call(Client::METHOD_GET, $path, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertNotEquals(403, $response['headers']['status-code'], 'Re-enabled REST protocol should not block ' . $service . ' endpoint');
}
}
public function testDisabledGraphqlBlocksMutationRequest(): void
{
$this->updateProtocolStatus('graphql', false);
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'query' => 'mutation { teamsCreate(teamId: "unique()", name: "Test") { _id } }',
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testResponseModel(): void
{
$response = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('protocolStatusForRest', $response['body']);
$this->assertArrayHasKey('protocolStatusForGraphql', $response['body']);
$this->assertArrayHasKey('protocolStatusForWebsocket', $response['body']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
// Failure flow
public function testUpdateProtocolWithoutAuthentication(): void
{
$response = $this->updateProtocolStatus('rest', false, false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateProtocolInvalidProtocolId(): void
{
$response = $this->updateProtocolStatus('invalid', false);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateProtocolEmptyProtocolId(): void
{
$response = $this->updateProtocolStatus('', false);
$this->assertSame(404, $response['headers']['status-code']);
}
// Helpers
protected function updateProtocolStatus(string $protocolId, bool $enabled, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId . '/status', $headers, [
'enabled' => $enabled,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ProtocolsConsoleClientTest extends Scope
{
use ProtocolsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class ProtocolsCustomServerTest extends Scope
{
use ProtocolsBase;
use ProjectCustom;
use SideServer;
}
+259
View File
@@ -0,0 +1,259 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Client;
trait ServicesBase
{
/**
* Optional services that can be toggled.
*/
protected static array $optionalServices = [
'account',
'avatars',
'databases',
'tablesdb',
'locale',
'health',
'project',
'storage',
'teams',
'users',
'vcs',
'sites',
'functions',
'proxy',
'migrations',
'messaging',
];
// Success flow
public function testDisableService(): void
{
foreach (self::$optionalServices as $service) {
$response = $this->updateServiceStatus($service, false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(false, $response['body']['serviceStatusFor' . ucfirst($service)]);
}
// Cleanup
foreach (self::$optionalServices as $service) {
$this->updateServiceStatus($service, true);
}
}
public function testEnableService(): void
{
// Disable first
foreach (self::$optionalServices as $service) {
$this->updateServiceStatus($service, false);
}
// Re-enable
foreach (self::$optionalServices as $service) {
$response = $this->updateServiceStatus($service, true);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(true, $response['body']['serviceStatusFor' . ucfirst($service)]);
}
}
public function testDisableServiceIdempotent(): void
{
$first = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(false, $first['body']['serviceStatusForTeams']);
$second = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(false, $second['body']['serviceStatusForTeams']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEnableServiceIdempotent(): void
{
$first = $this->updateServiceStatus('teams', true);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(true, $first['body']['serviceStatusForTeams']);
$second = $this->updateServiceStatus('teams', true);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(true, $second['body']['serviceStatusForTeams']);
}
public function testDisabledServiceBlocksClientRequest(): void
{
$this->updateServiceStatus('teams', false);
$response = $this->client->call(Client::METHOD_GET, '/teams', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_service_disabled', $response['body']['type']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEnabledServiceAllowsClientRequest(): void
{
$this->updateServiceStatus('teams', false);
$this->updateServiceStatus('teams', true);
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertSame(200, $response['headers']['status-code']);
}
public function testDisableOneServiceDoesNotAffectOther(): void
{
$this->updateServiceStatus('teams', false);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertSame(200, $response['headers']['status-code']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEachDisabledServiceBlocksItsEndpoint(): void
{
$serviceEndpoints = [
'account' => ['method' => Client::METHOD_GET, 'path' => '/account'],
'avatars' => ['method' => Client::METHOD_GET, 'path' => '/avatars/initials'],
'databases' => ['method' => Client::METHOD_GET, 'path' => '/databases'],
'tablesdb' => ['method' => Client::METHOD_GET, 'path' => '/tablesdb'],
'locale' => ['method' => Client::METHOD_GET, 'path' => '/locale'],
'health' => ['method' => Client::METHOD_GET, 'path' => '/health'],
'project' => ['method' => Client::METHOD_GET, 'path' => '/project/platforms'],
'storage' => ['method' => Client::METHOD_GET, 'path' => '/storage/buckets'],
'teams' => ['method' => Client::METHOD_GET, 'path' => '/teams'],
'users' => ['method' => Client::METHOD_GET, 'path' => '/users'],
'vcs' => ['method' => Client::METHOD_GET, 'path' => '/vcs/installations'],
'sites' => ['method' => Client::METHOD_GET, 'path' => '/sites'],
'functions' => ['method' => Client::METHOD_GET, 'path' => '/functions'],
'proxy' => ['method' => Client::METHOD_GET, 'path' => '/proxy/rules'],
'migrations' => ['method' => Client::METHOD_GET, 'path' => '/migrations'],
'messaging' => ['method' => Client::METHOD_GET, 'path' => '/messaging/providers'],
];
foreach ($serviceEndpoints as $service => $endpoint) {
$this->updateServiceStatus($service, false);
$response = $this->client->call($endpoint['method'], $endpoint['path'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code'], 'Service ' . $service . ' should block requests when disabled (got ' . $response['headers']['status-code'] . ')');
$this->assertSame('general_service_disabled', $response['body']['type'], 'Service ' . $service . ' should return general_service_disabled error type');
// Cleanup
$this->updateServiceStatus($service, true);
}
}
public function testEachReenabledServiceAllowsRequest(): void
{
$serviceEndpoints = [
'account' => ['method' => Client::METHOD_GET, 'path' => '/account'],
'avatars' => ['method' => Client::METHOD_GET, 'path' => '/avatars/initials'],
'databases' => ['method' => Client::METHOD_GET, 'path' => '/databases'],
'tablesdb' => ['method' => Client::METHOD_GET, 'path' => '/tablesdb'],
'locale' => ['method' => Client::METHOD_GET, 'path' => '/locale'],
'health' => ['method' => Client::METHOD_GET, 'path' => '/health'],
'project' => ['method' => Client::METHOD_GET, 'path' => '/project/platforms'],
'storage' => ['method' => Client::METHOD_GET, 'path' => '/storage/buckets'],
'teams' => ['method' => Client::METHOD_GET, 'path' => '/teams'],
'users' => ['method' => Client::METHOD_GET, 'path' => '/users'],
'vcs' => ['method' => Client::METHOD_GET, 'path' => '/vcs/installations'],
'sites' => ['method' => Client::METHOD_GET, 'path' => '/sites'],
'functions' => ['method' => Client::METHOD_GET, 'path' => '/functions'],
'proxy' => ['method' => Client::METHOD_GET, 'path' => '/proxy/rules'],
'migrations' => ['method' => Client::METHOD_GET, 'path' => '/migrations'],
'messaging' => ['method' => Client::METHOD_GET, 'path' => '/messaging/providers'],
];
foreach ($serviceEndpoints as $service => $endpoint) {
$this->updateServiceStatus($service, false);
$this->updateServiceStatus($service, true);
$response = $this->client->call($endpoint['method'], $endpoint['path'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertNotEquals(403, $response['headers']['status-code'], 'Service ' . $service . ' should allow requests after re-enabling');
}
}
public function testResponseModel(): void
{
$response = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('serviceStatusForTeams', $response['body']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
// Failure flow
public function testUpdateServiceWithoutAuthentication(): void
{
$response = $this->updateServiceStatus('teams', false, false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateServiceInvalidServiceId(): void
{
$response = $this->updateServiceStatus('invalid', false);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateServiceEmptyServiceId(): void
{
$response = $this->updateServiceStatus('', false);
$this->assertSame(404, $response['headers']['status-code']);
}
// Helpers
protected function updateServiceStatus(string $serviceId, bool $enabled, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId . '/status', $headers, [
'enabled' => $enabled,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ServicesConsoleClientTest extends Scope
{
use ServicesBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class ServicesCustomServerTest extends Scope
{
use ServicesBase;
use ProjectCustom;
use SideServer;
}
+5 -4
View File
@@ -90,16 +90,16 @@ trait ProjectsBase
'name' => 'Webhook Test',
'events' => ['users.*.create', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
self::$cachedProjectWithWebhook = array_merge($projectData, [
'webhookId' => $response['body']['$id'],
'signatureKey' => $response['body']['signatureKey']
'signatureKey' => $response['body']['secret']
]);
return self::$cachedProjectWithWebhook;
@@ -388,6 +388,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2497,49 +2497,222 @@ class ProjectsConsoleClientTest extends Scope
$id = $project['body']['$id'];
// Bulk disable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$matches = [];
$pattern = '/serviceStatusFor.*/';
foreach ($response['body'] as $key => $value) {
if (\preg_match($pattern, $key)) {
$matches[$key] = $value;
}
}
foreach ($matches as $value) {
$this->assertFalse($value);
}
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
// Bulk enable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => true,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
}
public function testUpdateProjectApisAll(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertNotEmpty($team['body']['$id']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertNotEmpty($project['body']['$id']);
$id = $project['body']['$id'];
// Bulk disable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => false,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
// Bulk enable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => true,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
}
public function testUpdateProjectApiStatus(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertNotEmpty($team['body']['$id']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertNotEmpty($project['body']['$id']);
$id = $project['body']['$id'];
$protocols = ['rest', 'graphql', 'websocket'];
/**
* Test for Disabled using old format (api + status)
*/
foreach ($protocols as $key) {
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => $key,
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(false, $response['body']['protocolStatusFor' . ucfirst($key)]);
}
/**
* Test for Enabled using old format (api + status)
*/
foreach ($protocols as $key) {
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => $key,
'status' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(true, $response['body']['protocolStatusFor' . ucfirst($key)]);
}
}
public function testUpdateProjectApiStatusRealtimeBackwardsCompat(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$id = $project['body']['$id'];
/**
* Test that "realtime" gets renamed to "websocket" via request filter
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => 'realtime',
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
@@ -2548,17 +2721,29 @@ class ProjectsConsoleClientTest extends Scope
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(false, $response['body']['protocolStatusForWebsocket']);
$matches = [];
foreach ($response['body'] as $key => $value) {
if (\preg_match($pattern, $key)) {
$matches[$key] = $value;
}
}
// Re-enable via old "realtime" name
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => 'realtime',
'status' => true,
]);
foreach ($matches as $value) {
$this->assertTrue($value);
}
$this->assertEquals(200, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(true, $response['body']['protocolStatusForWebsocket']);
}
public function testUpdateProjectServiceStatusAdmin(): array
@@ -2604,6 +2789,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2649,6 +2835,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $key,
'status' => true,
@@ -2678,6 +2865,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2725,6 +2913,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $service,
'status' => true,
@@ -2752,6 +2941,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2841,6 +3031,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $service,
'status' => true,
@@ -2862,9 +3053,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test',
'events' => ['users.*.create', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
@@ -2873,9 +3064,9 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.*.update.email', $response['body']['events']);
$this->assertCount(2, $response['body']['events']);
$this->assertEquals('https://appwrite.io', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(true, $response['body']['security']);
$this->assertEquals('username', $response['body']['httpUser']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(true, $response['body']['tls']);
$this->assertEquals('username', $response['body']['authUsername']);
/**
* Test for FAILURE
@@ -2889,9 +3080,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test',
'events' => ['account.unknown', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -2949,8 +3140,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.*.update.email', $response['body']['events']);
$this->assertCount(2, $response['body']['events']);
$this->assertEquals('https://appwrite.io', $response['body']['url']);
$this->assertEquals('username', $response['body']['httpUser']);
$this->assertEquals('password', $response['body']['httpPass']);
$this->assertEquals('username', $response['body']['authUsername']);
$this->assertEquals('password', $response['body']['authPassword']);
/**
* Test for FAILURE
@@ -2978,7 +3169,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
'url' => 'https://appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2990,10 +3181,10 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('buckets.*.files.*.create', $response['body']['events']);
$this->assertCount(3, $response['body']['events']);
$this->assertEquals('https://appwrite.io/new', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(false, $response['body']['security']);
$this->assertEquals('', $response['body']['httpUser']);
$this->assertEquals('', $response['body']['httpPass']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(false, $response['body']['tls']);
$this->assertEquals('', $response['body']['authUsername']);
$this->assertEquals('', $response['body']['authPassword']);
$response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
'content-type' => 'application/json',
@@ -3010,10 +3201,10 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('buckets.*.files.*.create', $response['body']['events']);
$this->assertCount(3, $response['body']['events']);
$this->assertEquals('https://appwrite.io/new', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(false, $response['body']['security']);
$this->assertEquals('', $response['body']['httpUser']);
$this->assertEquals('', $response['body']['httpPass']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(false, $response['body']['tls']);
$this->assertEquals('', $response['body']['authUsername']);
$this->assertEquals('', $response['body']['authPassword']);
/**
* Test for FAILURE
@@ -3026,7 +3217,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.unknown'],
'url' => 'https://appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -3039,7 +3230,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
'url' => 'appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -3064,15 +3255,15 @@ class ProjectsConsoleClientTest extends Scope
$webhookId = $data['webhookId'];
$signatureKey = $data['signatureKey'];
$response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
$response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-mode' => 'admin'
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['signatureKey']);
$this->assertNotEquals($signatureKey, $response['body']['signatureKey']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertNotEquals($signatureKey, $response['body']['secret']);
}
public function testDeleteProjectWebhook(): void
@@ -3091,9 +3282,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook To Delete',
'events' => ['users.*.create'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
+687 -68
View File
@@ -35,11 +35,11 @@ trait WebhooksBase
$this->assertContains('users.*.create', $webhook['body']['events']);
$this->assertCount(1, $webhook['body']['events']);
$this->assertEquals(true, $webhook['body']['enabled']);
$this->assertEquals(false, $webhook['body']['security']);
$this->assertEquals('', $webhook['body']['httpUser']);
$this->assertEquals('', $webhook['body']['httpPass']);
$this->assertNotEmpty($webhook['body']['signatureKey']);
$this->assertEquals(128, \strlen($webhook['body']['signatureKey']));
$this->assertEquals(false, $webhook['body']['tls']);
$this->assertEquals('', $webhook['body']['authUsername']);
$this->assertEquals('', $webhook['body']['authPassword']);
$this->assertNotEmpty($webhook['body']['secret']);
$this->assertEquals(128, \strlen($webhook['body']['secret']));
$this->assertEquals(0, $webhook['body']['attempts']);
$this->assertEquals('', $webhook['body']['logs']);
@@ -63,11 +63,11 @@ trait WebhooksBase
$this->deleteWebhook($webhook['body']['$id']);
}
public function testCreateWebhookWithSecurity(): void
public function testCreateWebhookWithTls(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Webhook With Security',
'Webhook With TLS',
['users.*.create'],
null,
'https://appwrite.io',
@@ -78,8 +78,8 @@ trait WebhooksBase
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertNotEmpty($webhook['body']['$id']);
$this->assertEquals(true, $webhook['body']['security']);
$this->assertIsBool($webhook['body']['security']);
$this->assertEquals(true, $webhook['body']['tls']);
$this->assertIsBool($webhook['body']['tls']);
// Cleanup
$this->deleteWebhook($webhook['body']['$id']);
@@ -100,14 +100,14 @@ trait WebhooksBase
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertNotEmpty($webhook['body']['$id']);
$this->assertEquals('username', $webhook['body']['httpUser']);
$this->assertEquals('password', $webhook['body']['httpPass']);
$this->assertEquals(true, $webhook['body']['security']);
$this->assertEquals('username', $webhook['body']['authUsername']);
$this->assertEquals('password', $webhook['body']['authPassword']);
$this->assertEquals(true, $webhook['body']['tls']);
// Verify via GET
$get = $this->getWebhook($webhook['body']['$id']);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('username', $get['body']['httpUser']);
$this->assertEquals('username', $get['body']['authUsername']);
// Cleanup
$this->deleteWebhook($webhook['body']['$id']);
@@ -331,11 +331,11 @@ trait WebhooksBase
$this->deleteWebhook($webhookId);
}
public function testUpdateWebhookWithSecurity(): void
public function testUpdateWebhookWithTls(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Security Webhook',
'TLS Webhook',
['users.*.create'],
null,
'https://appwrite.io',
@@ -345,7 +345,7 @@ trait WebhooksBase
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertEquals(false, $webhook['body']['security']);
$this->assertEquals(false, $webhook['body']['tls']);
$webhookId = $webhook['body']['$id'];
// Update to enable security
@@ -361,8 +361,8 @@ trait WebhooksBase
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(true, $updated['body']['security']);
$this->assertIsBool($updated['body']['security']);
$this->assertEquals(true, $updated['body']['tls']);
$this->assertIsBool($updated['body']['tls']);
// Cleanup
$this->deleteWebhook($webhookId);
@@ -382,8 +382,8 @@ trait WebhooksBase
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertEquals('', $webhook['body']['httpUser']);
$this->assertEquals('', $webhook['body']['httpPass']);
$this->assertEquals('', $webhook['body']['authUsername']);
$this->assertEquals('', $webhook['body']['authPassword']);
$webhookId = $webhook['body']['$id'];
// Update with HTTP auth credentials
@@ -399,13 +399,13 @@ trait WebhooksBase
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('newuser', $updated['body']['httpUser']);
$this->assertEquals('newpass', $updated['body']['httpPass']);
$this->assertEquals('newuser', $updated['body']['authUsername']);
$this->assertEquals('newpass', $updated['body']['authPassword']);
// Verify via GET
$get = $this->getWebhook($webhookId);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('newuser', $get['body']['httpUser']);
$this->assertEquals('newuser', $get['body']['authUsername']);
// Cleanup
$this->deleteWebhook($webhookId);
@@ -657,19 +657,19 @@ trait WebhooksBase
$this->assertContains('buckets.*.files.*.create', $updated['body']['events']);
$this->assertCount(3, $updated['body']['events']);
$this->assertEquals('https://appwrite.io/updated', $updated['body']['url']);
$this->assertEquals(true, $updated['body']['security']);
$this->assertEquals('user', $updated['body']['httpUser']);
$this->assertEquals('pass', $updated['body']['httpPass']);
$this->assertEquals(true, $updated['body']['tls']);
$this->assertEquals('user', $updated['body']['authUsername']);
$this->assertEquals('pass', $updated['body']['authPassword']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testUpdateWebhookSignature(): void
public function testUpdateWebhookSecret(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Signature Webhook',
'Secret Webhook',
['users.*.create'],
null,
'https://appwrite.io',
@@ -680,27 +680,27 @@ trait WebhooksBase
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
$originalSignatureKey = $webhook['body']['signatureKey'];
$originalSecret = $webhook['body']['secret'];
$this->assertNotEmpty($originalSignatureKey);
$this->assertEquals(128, \strlen($originalSignatureKey));
$this->assertNotEmpty($originalSecret);
$this->assertEquals(128, \strlen($originalSecret));
// Update signature
$updated = $this->updateWebhookSignature($webhookId);
// Update secret
$updated = $this->updateWebhookSecret($webhookId);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals($webhookId, $updated['body']['$id']);
$this->assertNotEmpty($updated['body']['signatureKey']);
$this->assertEquals(128, \strlen($updated['body']['signatureKey']));
$this->assertNotEquals($originalSignatureKey, $updated['body']['signatureKey']);
$this->assertNotEmpty($updated['body']['secret']);
$this->assertEquals(128, \strlen($updated['body']['secret']));
$this->assertNotEquals($originalSecret, $updated['body']['secret']);
// Verify new signature persisted via GET
// Verify secret is not exposed via GET
$get = $this->getWebhook($webhookId);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertNotEquals($originalSignatureKey, $get['body']['signatureKey']);
$this->assertEmpty($get['body']['secret']);
// Test signature update on non-existent webhook
$notFound = $this->updateWebhookSignature('non-existent-id');
// Test secret update on non-existent webhook
$notFound = $this->updateWebhookSecret('non-existent-id');
$this->assertEquals(404, $notFound['headers']['status-code']);
$this->assertEquals('webhook_not_found', $notFound['body']['type']);
@@ -708,6 +708,351 @@ trait WebhooksBase
$this->deleteWebhook($webhookId);
}
public function testSecretRotationZeroDowntime(): void
{
// Create webhook pointing to request-catcher so deliveries are captured
$webhook = $this->createWebhook(
ID::unique(),
'Rotation Test Webhook',
['users.*.create'],
null,
'http://request-catcher-webhook:5000/',
false,
null,
null
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
$originalSecret = $webhook['body']['secret'];
$this->assertNotEmpty($originalSecret);
$this->assertEquals(128, \strlen($originalSecret));
// Step 1: Trigger user creation with the original auto-generated secret
$email1 = uniqid() . 'rotation1@localhost.test';
$user1 = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => ID::unique(),
'email' => $email1,
'password' => 'password',
'name' => 'Rotation User 1',
]);
$this->assertEquals(201, $user1['headers']['status-code']);
$userId1 = $user1['body']['$id'];
// Verify webhook delivery is signed with the original secret
$this->assertEventually(function () use ($userId1, $originalSecret) {
$delivery = $this->getLastRequest(function (array $request) use ($userId1) {
$this->assertStringContainsString(
"users.{$userId1}.create",
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
);
});
$this->assertNotEmpty($delivery);
$payload = json_encode($delivery['data']);
$url = $delivery['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $originalSecret, true));
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
}, 15000, 500);
// Step 2: Rotate the secret to a known custom value
$newSecret = 'new-key-after-rotation';
$updated = $this->updateWebhookSecret($webhookId, $newSecret);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals($newSecret, $updated['body']['secret']);
$this->assertNotEquals($originalSecret, $updated['body']['secret']);
// Step 3: Trigger another user creation — should be signed with the new secret
$email2 = uniqid() . 'rotation2@localhost.test';
$user2 = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => ID::unique(),
'email' => $email2,
'password' => 'password',
'name' => 'Rotation User 2',
]);
$this->assertEquals(201, $user2['headers']['status-code']);
$userId2 = $user2['body']['$id'];
// Verify webhook delivery is signed with the new rotated secret
$this->assertEventually(function () use ($userId2, $newSecret) {
$delivery = $this->getLastRequest(function (array $request) use ($userId2) {
$this->assertStringContainsString(
"users.{$userId2}.create",
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
);
});
$this->assertNotEmpty($delivery);
$payload = json_encode($delivery['data']);
$url = $delivery['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $newSecret, true));
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
}, 15000, 500);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testCreateWebhookWithCustomSecret(): void
{
$customSecret = 'custom-secret-key';
// Create webhook with a custom secret pointing to request-catcher
$webhook = $this->createWebhook(
ID::unique(),
'Custom Secret Webhook',
['users.*.create'],
null,
'http://request-catcher-webhook:5000/',
false,
null,
null,
$customSecret
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
$this->assertEquals($customSecret, $webhook['body']['secret']);
// Trigger user creation to generate a webhook delivery
$email = uniqid() . 'customsecret@localhost.test';
$user = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => ID::unique(),
'email' => $email,
'password' => 'password',
'name' => 'Custom Secret User',
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'];
// Verify webhook delivery is signed with the custom secret
$this->assertEventually(function () use ($userId, $customSecret) {
$delivery = $this->getLastRequest(function (array $request) use ($userId) {
$this->assertStringContainsString(
"users.{$userId}.create",
$request['headers']['X-Appwrite-Webhook-Events'] ?? ''
);
});
$this->assertNotEmpty($delivery);
$payload = json_encode($delivery['data']);
$url = $delivery['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $customSecret, true));
$this->assertEquals($signatureExpected, $delivery['headers']['X-Appwrite-Webhook-Signature']);
}, 15000, 500);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testCreateWebhookSecretMinLength(): void
{
// 7 chars — below minimum of 8
$webhook = $this->createWebhook(
ID::unique(),
'Short Secret Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null,
'short12'
);
$this->assertEquals(400, $webhook['headers']['status-code']);
// 8 chars — exactly at minimum
$webhook = $this->createWebhook(
ID::unique(),
'Min Secret Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null,
'exact8ch'
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertEquals('exact8ch', $webhook['body']['secret']);
// Cleanup
$this->deleteWebhook($webhook['body']['$id']);
}
public function testCreateWebhookSecretMaxLength(): void
{
// 256 chars — exactly at maximum
$maxSecret = str_repeat('a', 256);
$webhook = $this->createWebhook(
ID::unique(),
'Max Secret Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null,
$maxSecret
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$this->assertEquals($maxSecret, $webhook['body']['secret']);
// Cleanup
$this->deleteWebhook($webhook['body']['$id']);
// 257 chars — above maximum
$tooLongSecret = str_repeat('a', 257);
$webhook = $this->createWebhook(
ID::unique(),
'Too Long Secret Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null,
$tooLongSecret
);
$this->assertEquals(400, $webhook['headers']['status-code']);
}
public function testUpdateWebhookSecretMinLength(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Secret Min Update Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
// 7 chars — below minimum of 8
$updated = $this->updateWebhookSecret($webhookId, 'short12');
$this->assertEquals(400, $updated['headers']['status-code']);
// 8 chars — exactly at minimum
$updated = $this->updateWebhookSecret($webhookId, 'exact8ch');
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('exact8ch', $updated['body']['secret']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testUpdateWebhookSecretMaxLength(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Secret Max Update Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
// 256 chars — exactly at maximum
$maxSecret = str_repeat('a', 256);
$updated = $this->updateWebhookSecret($webhookId, $maxSecret);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals($maxSecret, $updated['body']['secret']);
// 257 chars — above maximum
$tooLongSecret = str_repeat('a', 257);
$updated = $this->updateWebhookSecret($webhookId, $tooLongSecret);
$this->assertEquals(400, $updated['headers']['status-code']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testWebhookSecretNotExposedInResponses(): void
{
// Create webhook — secret IS returned on creation
$webhook = $this->createWebhook(
ID::unique(),
'Secret Exposure Test',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null,
'my-custom-secret'
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
$this->assertEquals('my-custom-secret', $webhook['body']['secret']);
$this->assertArrayNotHasKey('signatureKey', $webhook['body']);
// Get webhook — secret must not be exposed
$get = $this->getWebhook($webhookId);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEmpty($get['body']['secret']);
$this->assertArrayNotHasKey('signatureKey', $get['body']);
// List webhooks — secret must not be exposed
$list = $this->listWebhooks(null, true);
$this->assertEquals(200, $list['headers']['status-code']);
foreach ($list['body']['webhooks'] as $item) {
$this->assertEmpty($item['secret']);
$this->assertArrayNotHasKey('signatureKey', $item);
}
// Update webhook — secret must not be exposed
$updated = $this->updateWebhook(
$webhookId,
'Secret Exposure Test Updated',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEmpty($updated['body']['secret']);
$this->assertArrayNotHasKey('signatureKey', $updated['body']);
// Update webhook secret — secret IS returned on rotation
$rotated = $this->updateWebhookSecret($webhookId, 'rotated-secret-key');
$this->assertEquals(200, $rotated['headers']['status-code']);
$this->assertEquals('rotated-secret-key', $rotated['body']['secret']);
$this->assertArrayNotHasKey('signatureKey', $rotated['body']);
// Cleanup
$this->deleteWebhook($webhookId);
}
// URL validation tests
public function testCreateWebhookWithPrivateDomain(): void
@@ -883,6 +1228,12 @@ trait WebhooksBase
{
$customId = 'my-custom-webhook-id';
// Clean up stale webhook from a previous run if it exists
$existing = $this->getWebhook($customId);
if ($existing['headers']['status-code'] === 200) {
$this->deleteWebhook($customId);
}
$webhook = $this->createWebhook(
$customId,
'Custom ID Webhook',
@@ -902,6 +1253,19 @@ trait WebhooksBase
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals($customId, $get['body']['$id']);
// Ensure duplicate creation fails
$duplicate = $this->createWebhook(
$customId,
'Duplicate Custom ID Webhook',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(409, $duplicate['headers']['status-code']);
// Cleanup
$this->deleteWebhook($customId);
}
@@ -934,11 +1298,10 @@ trait WebhooksBase
$this->assertContains('users.*.update.email', $get['body']['events']);
$this->assertCount(2, $get['body']['events']);
$this->assertEquals(true, $get['body']['enabled']);
$this->assertEquals(true, $get['body']['security']);
$this->assertEquals('myuser', $get['body']['httpUser']);
$this->assertEquals('mypass', $get['body']['httpPass']);
$this->assertNotEmpty($get['body']['signatureKey']);
$this->assertEquals(128, \strlen($get['body']['signatureKey']));
$this->assertEquals(true, $get['body']['tls']);
$this->assertEquals('myuser', $get['body']['authUsername']);
$this->assertEquals('mypass', $get['body']['authPassword']);
$this->assertEmpty($get['body']['secret']);
$this->assertEquals(0, $get['body']['attempts']);
$this->assertEquals('', $get['body']['logs']);
@@ -1043,9 +1406,9 @@ trait WebhooksBase
$this->assertArrayHasKey('name', $webhook);
$this->assertArrayHasKey('url', $webhook);
$this->assertArrayHasKey('events', $webhook);
$this->assertArrayHasKey('security', $webhook);
$this->assertArrayHasKey('tls', $webhook);
$this->assertArrayHasKey('enabled', $webhook);
$this->assertArrayHasKey('signatureKey', $webhook);
$this->assertArrayHasKey('secret', $webhook);
$this->assertArrayHasKey('attempts', $webhook);
$this->assertArrayHasKey('logs', $webhook);
}
@@ -1247,11 +1610,11 @@ trait WebhooksBase
$this->deleteWebhook($webhook['body']['$id']);
}
public function testListWebhooksFilterBySecurity(): void
public function testListWebhooksFilterByTls(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'Security Filter Webhook',
'TLS Filter Webhook',
['users.*.create'],
null,
'https://appwrite.io/sec',
@@ -1262,13 +1625,13 @@ trait WebhooksBase
$this->assertEquals(201, $webhook['headers']['status-code']);
$list = $this->listWebhooks([
Query::equal('security', [true])->toString(),
Query::equal('tls', [true])->toString(),
], true);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
foreach ($list['body']['webhooks'] as $w) {
$this->assertEquals(true, $w['security']);
$this->assertEquals(true, $w['tls']);
}
// Cleanup
@@ -1503,6 +1866,254 @@ trait WebhooksBase
$this->assertEquals('webhook_not_found', $delete['body']['type']);
}
// =========================================================================
// Backward compatibility tests (1.9.0 response format)
// =========================================================================
public function testCreateWebhookV22BackwardCompatRequest(): void
{
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders());
// Send old param names with 1.9.0 header
$webhook = $this->client->call(Client::METHOD_POST, '/webhooks', $headers, [
'webhookId' => ID::unique(),
'name' => 'V22 Compat Create',
'events' => ['users.*.create'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'olduser',
'httpPass' => 'oldpass',
]);
$this->assertEquals(201, $webhook['headers']['status-code']);
// Response should use OLD field names
$this->assertArrayHasKey('security', $webhook['body']);
$this->assertArrayHasKey('httpUser', $webhook['body']);
$this->assertArrayHasKey('httpPass', $webhook['body']);
$this->assertArrayHasKey('signatureKey', $webhook['body']);
// New field names should NOT be present
$this->assertArrayNotHasKey('tls', $webhook['body']);
$this->assertArrayNotHasKey('authUsername', $webhook['body']);
$this->assertArrayNotHasKey('authPassword', $webhook['body']);
$this->assertArrayNotHasKey('secret', $webhook['body']);
// Values should be correct
$this->assertEquals(true, $webhook['body']['security']);
$this->assertEquals('olduser', $webhook['body']['httpUser']);
$this->assertEquals('oldpass', $webhook['body']['httpPass']);
$this->assertNotEmpty($webhook['body']['signatureKey']);
$this->assertEquals(128, \strlen($webhook['body']['signatureKey']));
// Cleanup
$this->deleteWebhook($webhook['body']['$id']);
}
public function testUpdateWebhookV22BackwardCompatRequest(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'V22 Compat Update',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders());
// Update using old param names
$updated = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, $headers, [
'name' => 'V22 Compat Updated',
'events' => ['users.*.create'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'updateduser',
'httpPass' => 'updatedpass',
]);
$this->assertEquals(200, $updated['headers']['status-code']);
// Response should use OLD field names
$this->assertArrayHasKey('security', $updated['body']);
$this->assertArrayHasKey('httpUser', $updated['body']);
$this->assertArrayHasKey('httpPass', $updated['body']);
$this->assertArrayHasKey('signatureKey', $updated['body']);
$this->assertArrayNotHasKey('tls', $updated['body']);
$this->assertArrayNotHasKey('authUsername', $updated['body']);
$this->assertArrayNotHasKey('authPassword', $updated['body']);
$this->assertArrayNotHasKey('secret', $updated['body']);
$this->assertEquals(true, $updated['body']['security']);
$this->assertEquals('updateduser', $updated['body']['httpUser']);
$this->assertEquals('updatedpass', $updated['body']['httpPass']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testGetWebhookV22BackwardCompatResponse(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'V22 Compat Get',
['users.*.create'],
null,
'https://appwrite.io',
true,
'getuser',
'getpass'
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
// GET with 1.9.0 header
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders());
$get = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, $headers);
$this->assertEquals(200, $get['headers']['status-code']);
// Should have old field names
$this->assertArrayHasKey('security', $get['body']);
$this->assertArrayHasKey('httpUser', $get['body']);
$this->assertArrayHasKey('httpPass', $get['body']);
$this->assertArrayHasKey('signatureKey', $get['body']);
$this->assertArrayNotHasKey('tls', $get['body']);
$this->assertArrayNotHasKey('authUsername', $get['body']);
$this->assertArrayNotHasKey('authPassword', $get['body']);
$this->assertArrayNotHasKey('secret', $get['body']);
$this->assertEquals(true, $get['body']['security']);
$this->assertEquals('getuser', $get['body']['httpUser']);
$this->assertEquals('getpass', $get['body']['httpPass']);
$this->assertEmpty($get['body']['signatureKey']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testListWebhooksV22BackwardCompatResponse(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'V22 Compat List',
['users.*.create'],
null,
'https://appwrite.io',
true,
'listuser',
'listpass'
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
// LIST with 1.9.0 header
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders());
$list = $this->client->call(Client::METHOD_GET, '/webhooks', $headers, [
'queries' => [
Query::equal('name', ['V22 Compat List'])->toString(),
],
'total' => true,
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertEquals(1, $list['body']['total']);
$this->assertCount(1, $list['body']['webhooks']);
$item = $list['body']['webhooks'][0];
// Each item should have old field names
$this->assertArrayHasKey('security', $item);
$this->assertArrayHasKey('httpUser', $item);
$this->assertArrayHasKey('httpPass', $item);
$this->assertArrayHasKey('signatureKey', $item);
$this->assertArrayNotHasKey('tls', $item);
$this->assertArrayNotHasKey('authUsername', $item);
$this->assertArrayNotHasKey('authPassword', $item);
$this->assertArrayNotHasKey('secret', $item);
$this->assertEquals(true, $item['security']);
$this->assertEquals('listuser', $item['httpUser']);
$this->assertEquals('listpass', $item['httpPass']);
// Cleanup
$this->deleteWebhook($webhookId);
}
public function testUpdateWebhookSecretV22BackwardCompatResponse(): void
{
$webhook = $this->createWebhook(
ID::unique(),
'V22 Compat Secret',
['users.*.create'],
null,
'https://appwrite.io',
null,
null,
null
);
$this->assertEquals(201, $webhook['headers']['status-code']);
$webhookId = $webhook['body']['$id'];
// Update secret with 1.9.0 header
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders());
$updated = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', $headers);
$this->assertEquals(200, $updated['headers']['status-code']);
// Response should use old field names
$this->assertArrayHasKey('signatureKey', $updated['body']);
$this->assertArrayHasKey('security', $updated['body']);
$this->assertArrayHasKey('httpUser', $updated['body']);
$this->assertArrayHasKey('httpPass', $updated['body']);
$this->assertArrayNotHasKey('secret', $updated['body']);
$this->assertArrayNotHasKey('tls', $updated['body']);
$this->assertArrayNotHasKey('authUsername', $updated['body']);
$this->assertArrayNotHasKey('authPassword', $updated['body']);
$this->assertNotEmpty($updated['body']['signatureKey']);
$this->assertEquals(128, \strlen($updated['body']['signatureKey']));
// Cleanup
$this->deleteWebhook($webhookId);
}
// Helpers
/**
@@ -1531,7 +2142,7 @@ trait WebhooksBase
return $webhook;
}
protected function createWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $security, ?string $httpUser, ?string $httpPass): mixed
protected function createWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $tls, ?string $authUsername, ?string $authPassword, ?string $secret = null): mixed
{
$params = [
'webhookId' => $webhookId,
@@ -1543,14 +2154,17 @@ trait WebhooksBase
if ($enabled !== null) {
$params['enabled'] = $enabled;
}
if ($security !== null) {
$params['security'] = $security;
if ($tls !== null) {
$params['tls'] = $tls;
}
if ($httpUser !== null) {
$params['httpUser'] = $httpUser;
if ($authUsername !== null) {
$params['authUsername'] = $authUsername;
}
if ($httpPass !== null) {
$params['httpPass'] = $httpPass;
if ($authPassword !== null) {
$params['authPassword'] = $authPassword;
}
if ($secret !== null) {
$params['secret'] = $secret;
}
$webhook = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
@@ -1561,7 +2175,7 @@ trait WebhooksBase
return $webhook;
}
protected function updateWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $security, ?string $httpUser, ?string $httpPass): mixed
protected function updateWebhook(string $webhookId, string $name, array $events, ?bool $enabled, ?string $url, ?bool $tls, ?string $authUsername, ?string $authPassword): mixed
{
$params = [
'name' => $name,
@@ -1572,14 +2186,14 @@ trait WebhooksBase
if ($enabled !== null) {
$params['enabled'] = $enabled;
}
if ($security !== null) {
$params['security'] = $security;
if ($tls !== null) {
$params['tls'] = $tls;
}
if ($httpUser !== null) {
$params['httpUser'] = $httpUser;
if ($authUsername !== null) {
$params['authUsername'] = $authUsername;
}
if ($httpPass !== null) {
$params['httpPass'] = $httpPass;
if ($authPassword !== null) {
$params['authPassword'] = $authPassword;
}
$webhook = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
@@ -1590,12 +2204,17 @@ trait WebhooksBase
return $webhook;
}
protected function updateWebhookSignature(string $webhookId): mixed
protected function updateWebhookSecret(string $webhookId, ?string $secret = null): mixed
{
$webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
$params = [];
if ($secret !== null) {
$params['secret'] = $secret;
}
$webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), $params);
return $webhook;
}
+3 -1
View File
@@ -4,6 +4,7 @@ namespace Tests\Unit\GraphQL;
use Appwrite\GraphQL\Types\Mapper;
use Appwrite\Utopia\Response;
use GraphQL\Type\Definition\NamedType;
use PHPUnit\Framework\TestCase;
use Swoole\Http\Response as SwooleResponse;
@@ -24,6 +25,7 @@ class BuilderTest extends TestCase
{
$model = $this->response->getModel(Response::MODEL_TABLE);
$type = Mapper::model(\ucfirst($model->getType()));
$this->assertEquals('Table', $type->name);
$this->assertInstanceOf(NamedType::class, $type);
$this->assertEquals('Table', $type->name());
}
}