mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc3febc38 | ||
|
|
8fcba8b5c9 | ||
|
|
6c8b34c230 | ||
|
|
377ade3c44 | ||
|
|
71533aaaf3 | ||
|
|
ccbe7e5d3f | ||
|
|
bea7739d7e | ||
|
|
76320d652a | ||
|
|
6d5968e2ea | ||
|
|
70c380fa36 | ||
|
|
512a7ae2bd | ||
|
|
49d3cd980f | ||
|
|
39a396810a | ||
|
|
1d307178bb | ||
|
|
c3ed2ff6ce | ||
|
|
efadf17bfe | ||
|
|
bcfec8d5de | ||
|
|
4b2e22d9da | ||
|
|
45e22e2243 | ||
|
|
0a21f3b139 | ||
|
|
cc8eb62c83 | ||
|
|
b1ad6ea87c | ||
|
|
d52d6c0bf0 | ||
|
|
db406b0a27 | ||
|
|
2585518e33 | ||
|
|
c9fceb870c | ||
|
|
9f1ec356d1 | ||
|
|
28d285d5c5 | ||
|
|
a941d8b855 | ||
|
|
3263133e5f | ||
|
|
a3ba66c90a | ||
|
|
c24d724000 | ||
|
|
df5ccc10ad | ||
|
|
2807d6cd9a | ||
|
|
20e2f2284f | ||
|
|
7a0d69c826 | ||
|
|
feedec80f2 | ||
|
|
584acafb1d | ||
|
|
27fc8058b9 | ||
|
|
c5bd8c712f | ||
|
|
a1267b1bff | ||
|
|
18d17ea945 | ||
|
|
fabd9559c4 | ||
|
|
ec637d4417 | ||
|
|
0cce480592 | ||
|
|
b622b092a8 | ||
|
|
96fe989f6d | ||
|
|
d13dbae0fe | ||
|
|
d69726487e | ||
|
|
5fccb8cc28 | ||
|
|
21a0d60c98 | ||
|
|
4eb8534294 | ||
|
|
c95f905bce | ||
|
|
0293da1e22 | ||
|
|
a4a0c4175d | ||
|
|
d3c73fbb49 | ||
|
|
8818187740 | ||
|
|
75324b24fc | ||
|
|
d6d118f4ab | ||
|
|
e998739998 | ||
|
|
31728c9b72 | ||
|
|
9d0fc9e5b1 | ||
|
|
a332eb5f32 | ||
|
|
775d21e5ee | ||
|
|
cf99269bc5 | ||
|
|
2a184288f4 | ||
|
|
9237f387fc | ||
|
|
e42a26fdad | ||
|
|
2c4770d29b | ||
|
|
2a9e423cb1 | ||
|
|
14869cc6d6 |
@@ -0,0 +1,29 @@
|
||||
# Patch Release Checklist for Appwrite
|
||||
|
||||
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
|
||||
|
||||
## Checklist
|
||||
|
||||
### Bump console image
|
||||
|
||||
Update the console Docker image tag in both files:
|
||||
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
|
||||
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
|
||||
|
||||
### Bump Appwrite version
|
||||
|
||||
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
|
||||
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
|
||||
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
|
||||
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
|
||||
|
||||
### Update CHANGES.md
|
||||
|
||||
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
|
||||
|
||||
## Final review
|
||||
|
||||
- [ ] Ask user to review changes before commiting
|
||||
- [ ] Ask user to update `CHANGES.md` with PRs
|
||||
- [ ] Ask user to generate specs, if needed
|
||||
- [ ] Ask user to add request and response filters, if needed
|
||||
@@ -512,7 +512,7 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
|
||||
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
## Patch release process
|
||||
|
||||
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
|
||||
|
||||
## Cross-repo context
|
||||
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -75,7 +75,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -88,7 +88,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -99,7 +99,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
||||
@@ -9,8 +9,8 @@ return [
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
],
|
||||
'realtime' => [
|
||||
'key' => 'realtime',
|
||||
'name' => 'Realtime',
|
||||
'websocket' => [
|
||||
'key' => 'websocket',
|
||||
'name' => 'Websocket',
|
||||
],
|
||||
];
|
||||
@@ -137,7 +137,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -193,7 +193,7 @@ return [
|
||||
'docs' => false,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -235,7 +235,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/proxy',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/proxy.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -291,7 +291,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
'tests' => true,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/migrations.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
|
||||
@@ -872,18 +872,18 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
|
||||
'introduction' => '1.7.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
|
||||
@@ -231,7 +231,7 @@ function execute(
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
|
||||
@@ -71,202 +71,20 @@ Http::get('/v1/projects/:projectId')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/service')
|
||||
->desc('Update service status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateServiceStatus',
|
||||
description: '/docs/references/projects/update-service-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$services = $project->getAttribute('services', []);
|
||||
$services[$service] = $status;
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/service/all')
|
||||
->desc('Update all service status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateServiceStatusAll',
|
||||
description: '/docs/references/projects/update-service-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
|
||||
|
||||
$services = [];
|
||||
foreach ($allServices as $service) {
|
||||
$services[$service] = $status;
|
||||
}
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/api')
|
||||
->desc('Update API status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateApiStatus',
|
||||
description: '/docs/references/projects/update-api-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatus',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateAPIStatus',
|
||||
description: '/docs/references/projects/update-api-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
|
||||
->param('status', null, new Boolean(), 'API status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$apis = $project->getAttribute('apis', []);
|
||||
$apis[$api] = $status;
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
->action(function () {
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/api/all')
|
||||
->desc('Update all API status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateApiStatusAll',
|
||||
description: '/docs/references/projects/update-api-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatusAll',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateAPIStatusAll',
|
||||
description: '/docs/references/projects/update-api-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('status', null, new Boolean(), 'API status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allApis = array_keys(Config::getParam('apis'));
|
||||
|
||||
$apis = [];
|
||||
foreach ($allApis as $api) {
|
||||
$apis[$api] = $status;
|
||||
}
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
->action(function () {
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/oauth2')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
|
||||
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
|
||||
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
|
||||
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
|
||||
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
|
||||
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
|
||||
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
|
||||
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
|
||||
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
|
||||
|
||||
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4321;
|
||||
const APP_VERSION_STABLE = '1.9.0';
|
||||
const APP_CACHE_BUSTER = 4322;
|
||||
const APP_VERSION_STABLE = '1.9.1';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
||||
+4
-2
@@ -653,9 +653,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$user = $connectionContainer->get('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
$apis = $project->getAttribute('apis', []);
|
||||
// Websocket is what to check, but realtime is checked too for backwards compatibility
|
||||
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
!$websocketEnabled
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@
|
||||
"chillerlan/php-qrcode": "4.3.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
|
||||
Generated
+40
-26
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,26 @@ class Realtime extends Event
|
||||
return $this->subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the event state for long-lived worker processes.
|
||||
*
|
||||
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
|
||||
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
|
||||
* to prevent stale state from affecting subsequent triggers.
|
||||
*/
|
||||
public function reset(): self
|
||||
{
|
||||
parent::reset();
|
||||
|
||||
$this->subscribers = [];
|
||||
$this->context = [];
|
||||
$this->project = null;
|
||||
$this->user = null;
|
||||
$this->userId = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Event.
|
||||
*
|
||||
|
||||
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @param iterable $promisesOrValues
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function all(array $promisesOrValues): GQLPromise;
|
||||
abstract public function all(iterable $promisesOrValues): GQLPromise;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,12 @@ class Swoole extends Adapter
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
public function all(iterable $promisesOrValues): GQLPromise
|
||||
{
|
||||
if ($promisesOrValues instanceof \Traversable) {
|
||||
$promisesOrValues = \iterator_to_array($promisesOrValues);
|
||||
}
|
||||
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
public string $name = 'Assoc';
|
||||
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
@@ -30,6 +31,10 @@ class Assoc extends Json
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
return \json_decode($valueNode->value, true);
|
||||
if ($valueNode instanceof StringValueNode) {
|
||||
return \json_decode($valueNode->value, true);
|
||||
}
|
||||
|
||||
return parent::parseLiteral($valueNode, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
public $name = 'InputFile';
|
||||
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
public string $name = 'InputFile';
|
||||
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Json extends ScalarType
|
||||
{
|
||||
public $name = 'Json';
|
||||
public $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
public string $name = 'Json';
|
||||
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -93,6 +93,7 @@ abstract class Migration
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+24
-7
@@ -50,6 +50,11 @@ class Create extends Action
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
@@ -139,30 +144,42 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
: $data;
|
||||
|
||||
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
|
||||
$hasData = !empty($data);
|
||||
$hasDocuments = !empty($documents);
|
||||
|
||||
/**
|
||||
* Determine which internal path to call, single or bulk
|
||||
*/
|
||||
if (empty($data) && empty($documents)) {
|
||||
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
// No single or bulk documents provided
|
||||
throw new Exception($this->getMissingDataException());
|
||||
}
|
||||
if (!empty($data) && !empty($documents)) {
|
||||
|
||||
// When empty documents are supported, an empty payload should still be treated as single create.
|
||||
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
$data = [];
|
||||
$hasData = true;
|
||||
}
|
||||
|
||||
if ($hasData && $hasDocuments) {
|
||||
// Both single and bulk documents provided
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
|
||||
}
|
||||
if (!empty($data) && empty($documentId)) {
|
||||
if ($hasData && empty($documentId)) {
|
||||
// Single document provided without document ID
|
||||
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
|
||||
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
|
||||
throw new Exception($this->getMissingDataException(), $message);
|
||||
}
|
||||
if (!empty($documents) && !empty($documentId)) {
|
||||
if ($hasDocuments && !empty($documentId)) {
|
||||
// Bulk documents provided with document ID
|
||||
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
|
||||
throw new Exception(
|
||||
@@ -170,13 +187,13 @@ class Create extends Action
|
||||
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
|
||||
);
|
||||
}
|
||||
if (!empty($documents) && !empty($permissions)) {
|
||||
if ($hasDocuments && !empty($permissions)) {
|
||||
// Bulk documents provided with permissions
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
|
||||
}
|
||||
|
||||
$isBulk = true;
|
||||
if (!empty($data)) {
|
||||
$isBulk = $hasDocuments;
|
||||
if ($hasData) {
|
||||
// Single document provided, convert to single item array
|
||||
// But remember that it was single to respond with a single document
|
||||
$isBulk = false;
|
||||
|
||||
+6
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends DocumentLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBDocumentLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: 'logs',
|
||||
name: 'listDocumentLogs',
|
||||
description: '/docs/references/documentsdb/get-document-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends CollectionLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBCollectionLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'listCollectionLogs',
|
||||
description: '/docs/references/documentsdb/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends DocumentLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listVectorsDBDocumentLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorsDB',
|
||||
group: 'logs',
|
||||
name: 'listDocumentLogs',
|
||||
description: '/docs/references/vectorsdb/get-document-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends CollectionLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listVectorsDBCollectionLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorsDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'listCollectionLogs',
|
||||
description: '/docs/references/vectorsdb/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
|
||||
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
|
||||
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
|
||||
$service->addAction(UpdateTable::getName(), new UpdateTable());
|
||||
$service->addAction(DeleteTable::getName(), new DeleteTable());
|
||||
$service->addAction(ListTables::getName(), new ListTables());
|
||||
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
|
||||
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
|
||||
}
|
||||
|
||||
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
|
||||
$service->addAction(DeleteRow::getName(), new DeleteRow());
|
||||
$service->addAction(DeleteRows::getName(), new DeleteRows());
|
||||
$service->addAction(ListRows::getName(), new ListRows());
|
||||
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
|
||||
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
|
||||
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
|
||||
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
|
||||
@@ -69,7 +67,6 @@ class VectorsDB extends Base
|
||||
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
|
||||
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
|
||||
$service->addAction(ListCollections::getName(), new ListCollections());
|
||||
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
|
||||
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
|
||||
}
|
||||
|
||||
@@ -92,7 +89,6 @@ class VectorsDB extends Base
|
||||
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
|
||||
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
|
||||
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
|
||||
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
|
||||
}
|
||||
|
||||
private function registerTransactionActions(Service $service): void
|
||||
|
||||
@@ -650,26 +650,30 @@ class Databases extends Action
|
||||
Document|null $attribute = null,
|
||||
Document|null $index = null,
|
||||
): void {
|
||||
$queueForRealtime
|
||||
->setProject($project)
|
||||
->setSubscribers(['console'])
|
||||
->setEvent($event)
|
||||
->setParam('databaseId', $database->getId())
|
||||
->setParam('tableId', $collection->getId())
|
||||
->setParam('collectionId', $collection->getId());
|
||||
try {
|
||||
$queueForRealtime
|
||||
->setProject($project)
|
||||
->setSubscribers(['console'])
|
||||
->setEvent($event)
|
||||
->setParam('databaseId', $database->getId())
|
||||
->setParam('tableId', $collection->getId())
|
||||
->setParam('collectionId', $collection->getId());
|
||||
|
||||
if (! empty($attribute)) {
|
||||
$queueForRealtime
|
||||
->setParam('columnId', $attribute->getId())
|
||||
->setParam('attributeId', $attribute->getId())
|
||||
->setPayload($attribute->getArrayCopy());
|
||||
}
|
||||
if (! empty($index)) {
|
||||
$queueForRealtime
|
||||
->setParam('indexId', $index->getId())
|
||||
->setPayload($index->getArrayCopy());
|
||||
if (! empty($attribute)) {
|
||||
$queueForRealtime
|
||||
->setParam('columnId', $attribute->getId())
|
||||
->setParam('attributeId', $attribute->getId())
|
||||
->setPayload($attribute->getArrayCopy());
|
||||
}
|
||||
if (! empty($index)) {
|
||||
$queueForRealtime
|
||||
->setParam('indexId', $index->getId())
|
||||
->setPayload($index->getArrayCopy());
|
||||
}
|
||||
$queueForRealtime->trigger();
|
||||
} finally {
|
||||
$queueForRealtime->reset();
|
||||
}
|
||||
|
||||
$queueForRealtime->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -15,6 +15,8 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
@@ -22,15 +24,15 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateWebhookSignature';
|
||||
return 'updateWebhookSecret';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/webhooks/:webhookId/signature')
|
||||
->setHttpPath('/v1/webhooks/:webhookId/secret')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
||||
->desc('Update webhook signature key')
|
||||
->desc('Update webhook secret key')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.write')
|
||||
->label('event', 'webhooks.[webhookId].update')
|
||||
@@ -39,9 +41,9 @@ class Update extends Action
|
||||
->label('sdk', new Method(
|
||||
namespace: 'webhooks',
|
||||
group: null,
|
||||
name: 'updateSignature',
|
||||
name: 'updateSecret',
|
||||
description: <<<EOT
|
||||
Update the webhook signature key. This endpoint can be used to regenerate the signature key used to sign and validate payload deliveries for a specific webhook.
|
||||
Update the webhook signing key. This endpoint can be used to regenerate the signing key used to sign and validate payload deliveries for a specific webhook.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
@@ -52,6 +54,7 @@ class Update extends Action
|
||||
]
|
||||
))
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
|
||||
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
@@ -62,6 +65,7 @@ class Update extends Action
|
||||
|
||||
public function action(
|
||||
string $webhookId,
|
||||
?string $secret,
|
||||
Response $response,
|
||||
Document $project,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -78,7 +82,7 @@ class Update extends Action
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
|
||||
]);
|
||||
|
||||
$webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
|
||||
@@ -63,9 +63,9 @@ class Update extends Action
|
||||
->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
|
||||
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
|
||||
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
|
||||
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
|
||||
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
@@ -80,9 +80,9 @@ class Update extends Action
|
||||
string $url,
|
||||
array $events,
|
||||
bool $enabled,
|
||||
bool $security,
|
||||
string $httpUser,
|
||||
string $httpPass,
|
||||
bool $tls,
|
||||
string $authUsername,
|
||||
string $authPassword,
|
||||
Response $response,
|
||||
Document $project,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -102,9 +102,9 @@ class Update extends Action
|
||||
'name' => $name,
|
||||
'events' => $events,
|
||||
'url' => $url,
|
||||
'security' => $security,
|
||||
'httpUser' => $httpUser,
|
||||
'httpPass' => $httpPass,
|
||||
'security' => $tls,
|
||||
'httpUser' => $authUsername,
|
||||
'httpPass' => $authPassword,
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
@@ -118,6 +118,8 @@ class Update extends Action
|
||||
|
||||
$queueForEvents->setParam('webhookId', $webhook->getId());
|
||||
|
||||
$webhook->removeAttribute('signatureKey');
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,15 @@ class XList extends Action
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$attribute = $query->getAttribute();
|
||||
if ($attribute === 'authUsername') {
|
||||
$query->setAttribute('httpUser');
|
||||
} elseif ($attribute === 'tls') {
|
||||
$query->setAttribute('security');
|
||||
}
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
@@ -111,6 +120,10 @@ class XList extends Action
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
foreach ($webhooks as $webhook) {
|
||||
$webhook->removeAttribute('signatureKey');
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'webhooks' => $webhooks,
|
||||
'total' => $total,
|
||||
|
||||
@@ -6,7 +6,7 @@ use Appwrite\Platform\Modules\Webhooks\Http\Init;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Create as CreateWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Delete as DeleteWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Get as GetWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature\Update as UpdateWebhookSignature;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret\Update as UpdateWebhookSecret;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Update as UpdateWebhook;
|
||||
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\XList as ListWebhooks;
|
||||
use Utopia\Platform\Service;
|
||||
@@ -26,6 +26,6 @@ class Http extends Service
|
||||
$this->addAction(GetWebhook::getName(), new GetWebhook());
|
||||
$this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
|
||||
$this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
|
||||
$this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
|
||||
$this->addAction(UpdateWebhookSecret::getName(), new UpdateWebhookSecret());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Webhook extends Model
|
||||
{
|
||||
@@ -50,27 +51,27 @@ class Webhook extends Model
|
||||
],
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('security', [
|
||||
->addRule('tls', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Indicated if SSL / TLS Certificate verification is enabled.',
|
||||
'description' => 'Indicates if SSL / TLS certificate verification is enabled.',
|
||||
'default' => true,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('httpUser', [
|
||||
->addRule('authUsername', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'HTTP basic authentication username.',
|
||||
'default' => '',
|
||||
'example' => 'username',
|
||||
])
|
||||
->addRule('httpPass', [
|
||||
->addRule('authPassword', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'HTTP basic authentication password.',
|
||||
'default' => '',
|
||||
'example' => 'password',
|
||||
])
|
||||
->addRule('signatureKey', [
|
||||
->addRule('secret', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Signature key which can be used to validated incoming',
|
||||
'description' => 'Signature key which can be used to validate incoming webhook payloads. Only returned on creation and secret rotation.',
|
||||
'default' => '',
|
||||
'example' => 'ad3d581ca230e2b7059c545e5a',
|
||||
])
|
||||
@@ -94,6 +95,23 @@ class Webhook extends Model
|
||||
]);
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
$document->setAttribute('tls', $document->getAttribute('security'));
|
||||
$document->removeAttribute('security');
|
||||
|
||||
$document->setAttribute('authUsername', $document->getAttribute('httpUser'));
|
||||
$document->removeAttribute('httpUser');
|
||||
|
||||
$document->setAttribute('authPassword', $document->getAttribute('httpPass'));
|
||||
$document->removeAttribute('httpPass');
|
||||
|
||||
$document->setAttribute('secret', $document->getAttribute('signatureKey'));
|
||||
$document->removeAttribute('signatureKey');
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
|
||||
@@ -22,7 +22,7 @@ class Comment
|
||||
'Every Git commit and branch gets its own deployment URL automatically',
|
||||
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
|
||||
'HTTPS and SSL certificates are handled automatically for all your Sites',
|
||||
'Functions can run for up to 15 minutes before timing out',
|
||||
'Function builds can take up to 45 minutes before timing out',
|
||||
'Schedule functions to run as often as every minute with cron expressions',
|
||||
'Environment variables can be scoped per function or shared across your project',
|
||||
'Function scopes give you fine-grained control over API permissions',
|
||||
|
||||
@@ -216,7 +216,7 @@ trait ProjectCustom
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://request-catcher-webhook:5000/',
|
||||
'security' => false,
|
||||
'tls' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
@@ -243,7 +243,7 @@ trait ProjectCustom
|
||||
'apiKey' => $key['body']['secret'],
|
||||
'devKey' => $devKey['body']['secret'],
|
||||
'webhookId' => $webhook['body']['$id'],
|
||||
'signatureKey' => $webhook['body']['signatureKey'],
|
||||
'signatureKey' => $webhook['body']['secret'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user