mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
552b6ab7ff | ||
|
|
7a0d69c826 | ||
|
|
feedec80f2 | ||
|
|
f4d40a1289 | ||
|
|
1e65f075e6 | ||
|
|
68725d9262 | ||
|
|
5b805d686b | ||
|
|
584acafb1d | ||
|
|
dce7856b77 | ||
|
|
a6af609317 | ||
|
|
035f6244e1 | ||
|
|
723cb1a488 | ||
|
|
815209ebb0 | ||
|
|
53c74582fc | ||
|
|
78bbe77580 | ||
|
|
0c3871a681 | ||
|
|
b59eba4ec6 | ||
|
|
cb4c97f2ee | ||
|
|
2ee2ea09a0 | ||
|
|
4ec84a147f | ||
|
|
e583de4650 | ||
|
|
98af2a5eb3 | ||
|
|
e3ad0f85de | ||
|
|
a26382ac51 | ||
|
|
9ba182d8a0 | ||
|
|
27fc8058b9 | ||
|
|
c5bd8c712f | ||
|
|
a1267b1bff | ||
|
|
18d17ea945 | ||
|
|
fabd9559c4 | ||
|
|
ec637d4417 | ||
|
|
4523e86b91 | ||
|
|
1ea108c2ce | ||
|
|
0a864e51b8 | ||
|
|
c6dd7de216 | ||
|
|
f77a64bff9 | ||
|
|
f552a1ba15 | ||
|
|
7282c5d51f | ||
|
|
82ec75d582 | ||
|
|
6bf6142667 | ||
|
|
938e65cb02 | ||
|
|
114de91f48 | ||
|
|
2e6f3f5c14 | ||
|
|
7b3d9bae03 | ||
|
|
d81a1154e3 | ||
|
|
a944c65660 | ||
|
|
856046dc82 | ||
|
|
2ca551123d | ||
|
|
c861d45749 | ||
|
|
4a43969da9 | ||
|
|
6fa4122910 | ||
|
|
1d27101770 | ||
|
|
9214decc8d | ||
|
|
ee4ae3bd47 | ||
|
|
53a114e7e4 | ||
|
|
d69726487e | ||
|
|
5fccb8cc28 | ||
|
|
21a0d60c98 | ||
|
|
4eb8534294 | ||
|
|
c95f905bce | ||
|
|
0293da1e22 | ||
|
|
a4a0c4175d | ||
|
|
d3c73fbb49 | ||
|
|
8818187740 | ||
|
|
75324b24fc | ||
|
|
d6d118f4ab | ||
|
|
e998739998 | ||
|
|
912dbda159 | ||
|
|
410a050244 | ||
|
|
9cf45816c2 | ||
|
|
920ddd18e6 | ||
|
|
bf489ce13b | ||
|
|
357d6482f9 | ||
|
|
da2444ede2 | ||
|
|
bc224de751 | ||
|
|
ca62504b5a | ||
|
|
d5fe5c34af | ||
|
|
6bc9adece8 | ||
|
|
97d46c6273 | ||
|
|
9d78a8e6b6 | ||
|
|
d12a6f5168 | ||
|
|
0f47e6ea28 | ||
|
|
592629587d | ||
|
|
187fde4a4e | ||
|
|
bfbf180aee | ||
|
|
d8a3b53641 | ||
|
|
df4dbcf607 | ||
|
|
29b0ebb3bd | ||
|
|
f0ccd1f586 |
@@ -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
|
||||
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
## Patch release process
|
||||
|
||||
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
|
||||
|
||||
## Cross-repo context
|
||||
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -75,7 +75,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -88,7 +88,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -99,7 +99,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
||||
+5
-4
@@ -6,8 +6,8 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
@@ -253,9 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Message\Migration as MigrationMessage;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -90,10 +91,9 @@ Http::post('/v1/migrations/appwrite')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -114,12 +114,11 @@ Http::post('/v1/migrations/appwrite')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -151,10 +150,9 @@ Http::post('/v1/migrations/firebase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$serviceAccountData = json_decode($serviceAccount, true);
|
||||
|
||||
if (empty($serviceAccountData)) {
|
||||
@@ -183,12 +181,11 @@ Http::post('/v1/migrations/firebase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -225,10 +222,9 @@ Http::post('/v1/migrations/supabase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -252,12 +248,11 @@ Http::post('/v1/migrations/supabase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -295,10 +290,9 @@ Http::post('/v1/migrations/nhost')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -323,12 +317,11 @@ Http::post('/v1/migrations/nhost')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -368,7 +361,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -383,7 +376,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -479,11 +472,10 @@ Http::post('/v1/migrations/csv/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -526,7 +518,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -545,7 +537,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -630,11 +622,11 @@ Http::post('/v1/migrations/csv/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -673,7 +665,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -688,7 +680,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -783,11 +775,11 @@ Http::post('/v1/migrations/json/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -826,7 +818,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -841,7 +833,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -915,11 +907,11 @@ Http::post('/v1/migrations/json/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -1216,9 +1208,8 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
@@ -1234,12 +1225,11 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->setAttribute('dateUpdated', \time());
|
||||
|
||||
// Trigger Migration
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
+10
-12
@@ -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());
|
||||
}
|
||||
@@ -1168,15 +1176,6 @@ Http::error()
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$trace = $error->getTrace();
|
||||
|
||||
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
|
||||
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
|
||||
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
|
||||
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
|
||||
Console::error("[$index] $file : $line -> $function()");
|
||||
}
|
||||
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1186,9 +1185,7 @@ Http::error()
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
Span::error($error);
|
||||
}
|
||||
Span::error($error);
|
||||
|
||||
switch ($class) {
|
||||
case Utopia\Http\Exception::class:
|
||||
@@ -1430,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
|
||||
|
||||
@@ -37,6 +37,7 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -424,7 +425,7 @@ Http::init()
|
||||
}
|
||||
|
||||
if (! empty($method)) {
|
||||
$namespace = $method->getNamespace();
|
||||
$namespace = \strtolower($method->getNamespace());
|
||||
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
@@ -435,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))) {
|
||||
@@ -510,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
|
||||
*/
|
||||
@@ -633,6 +635,7 @@ Http::init()
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
Span::add('storage.cache.key', $key);
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
@@ -681,6 +684,8 @@ Http::init()
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
Span::add('storage.bucket.id', $bucketId);
|
||||
Span::add('storage.file.id', $fileId);
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
@@ -708,10 +713,12 @@ Http::init()
|
||||
->setContentType($cacheLog->getAttribute('mimeType'));
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
|
||||
if (! $isImageTransformation || ! $isDisabled) {
|
||||
Span::add('storage.cache.hit', true);
|
||||
$response->send($data);
|
||||
}
|
||||
} else {
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
|
||||
Span::add('storage.cache.hit', false);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/**
|
||||
* Register the minimal per-connection resources required by realtime.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$getProjectId = static function (Request $request): string {
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
|
||||
if (!empty($projectId)) {
|
||||
return $projectId;
|
||||
}
|
||||
|
||||
$projectId = $request->getParam('project', '');
|
||||
|
||||
return \is_string($projectId) ? $projectId : '';
|
||||
};
|
||||
|
||||
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
|
||||
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (!empty($projectId) && $project->getId() !== $projectId) {
|
||||
$mode = APP_MODE_ADMIN;
|
||||
}
|
||||
|
||||
return $mode;
|
||||
};
|
||||
|
||||
$getDbForPlatform = static function (Authorization $authorization) {
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $getDbForPlatform($authorization);
|
||||
}
|
||||
|
||||
$database = getProjectDB($project);
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
|
||||
if (empty($domain)) {
|
||||
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
|
||||
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($domain));
|
||||
}
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]) ?? new Document();
|
||||
});
|
||||
|
||||
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
|
||||
|
||||
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
|
||||
$trustedProjects = [];
|
||||
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
|
||||
if (empty($trustedProject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trustedProjects[] = $trustedProject;
|
||||
}
|
||||
|
||||
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
|
||||
$permitsCurrentProject = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$permitsCurrentProject) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
return $rule;
|
||||
};
|
||||
|
||||
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
|
||||
$key = $project->find('secret', $devKey, 'devKeys');
|
||||
|
||||
if (!$key) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$accessedAt = $key->getAttribute('accessedAt', 0);
|
||||
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
|
||||
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (!\in_array($sdk, $sdks, true)) {
|
||||
$sdks[] = $sdk;
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'sdks' => $key->getAttribute('sdks'),
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
};
|
||||
|
||||
$container->set('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
|
||||
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
}, ['request', 'console', 'authorization']);
|
||||
|
||||
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
|
||||
$devKey = $findDevKey($request, $project, $servers, $authorization);
|
||||
|
||||
if (!$devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
|
||||
$allowedHostnames = [...($platform['hostnames'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
$rule = $findRule($request, $project, $authorization);
|
||||
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
|
||||
$allowedHostnames[] = $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
$hostname = $originHostname ?: $refererHostname;
|
||||
|
||||
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
|
||||
$allowedHostnames[] = $hostname;
|
||||
}
|
||||
|
||||
$allowedSchemes = [...($platform['schemas'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedSchemes[] = 'exp';
|
||||
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
|
||||
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
|
||||
}, ['platform', 'request', 'project', 'servers', 'authorization']);
|
||||
|
||||
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
|
||||
$mode = $getMode($request, $project);
|
||||
$store = new Store();
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$dbForProject = $getDbForProject($project, $authorization);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
$store->decode(
|
||||
$request->getCookie(
|
||||
$store->getKey(),
|
||||
$request->getCookie($store->getKey() . '_legacy', '')
|
||||
)
|
||||
);
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
$store->decode($sessionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
|
||||
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new User([]);
|
||||
} elseif (!empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$user
|
||||
|| $user->isEmpty()
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) {
|
||||
$user = new User([]);
|
||||
}
|
||||
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
if (!empty($authJWT) && !$project->isEmpty()) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$payload = $jwt->decode($authJWT);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
|
||||
$user = new User([]);
|
||||
}
|
||||
}
|
||||
|
||||
$accountKey = $request->getHeader('x-appwrite-key', '');
|
||||
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
|
||||
|
||||
if (!empty($accountKeyUserId) && !empty($accountKey)) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (!$accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
key: 'secret',
|
||||
find: $accountKey,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!empty($key)) {
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
$user = $accountKeyUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
|
||||
if (!empty($impersonateUserId)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
|
||||
} elseif (!empty($impersonateEmail)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('email', [\strtolower($impersonateEmail)]),
|
||||
]));
|
||||
} elseif (!empty($impersonatePhone)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('phone', [$impersonatePhone]),
|
||||
]));
|
||||
}
|
||||
|
||||
if ($targetUser !== null && !$targetUser->isEmpty()) {
|
||||
$impersonator = clone $user;
|
||||
$user = clone $targetUser;
|
||||
$user->setAttribute('impersonatorUserId', $impersonator->getId());
|
||||
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
|
||||
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
|
||||
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
|
||||
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
|
||||
}
|
||||
}
|
||||
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['request', 'project', 'console', 'authorization']);
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
@@ -82,6 +85,18 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForExecutions', fn (Publisher $publisher) => new ExecutionPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
|
||||
/**
|
||||
* Platform configuration
|
||||
|
||||
@@ -13,10 +13,8 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
@@ -163,13 +161,6 @@ return function (Container $container): void {
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -9,7 +9,6 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
@@ -344,10 +343,6 @@ return function (Container $container): void {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
+151
-43
@@ -35,8 +35,6 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Adapter\FPM\Server as HttpServer;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
@@ -45,12 +43,12 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
use Utopia\WebSocket\Server;
|
||||
|
||||
/**
|
||||
* @var Registry $register
|
||||
*/
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
$registerRequestResources ??= require __DIR__ . '/init/resources/request.php';
|
||||
/** @var Registry $register */
|
||||
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
|
||||
|
||||
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
@@ -450,7 +448,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
]
|
||||
];
|
||||
|
||||
$server->send($realtime->getSubscribers($event), json_encode([
|
||||
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
]));
|
||||
@@ -557,7 +555,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
if (Http::isDevelopment() && !empty($receivers)) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
|
||||
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
|
||||
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
|
||||
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
|
||||
@@ -623,7 +621,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) {
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
|
||||
global $container;
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -631,14 +629,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
$connectionContainer = new Container($container);
|
||||
|
||||
$adapter = new HttpServer($connectionContainer);
|
||||
$app = new Http($adapter, 'UTC');
|
||||
$connectionContainer->set('utopia', fn () => $app);
|
||||
$connectionContainer->set('request', fn () => $request);
|
||||
$connectionContainer->set('response', fn () => $response);
|
||||
|
||||
$registerRequestResources($connectionContainer);
|
||||
$registerConnectionResources($connectionContainer);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
@@ -646,8 +639,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
$authorization = $app->getResource('authorization');
|
||||
$project = $connectionContainer->get('project');
|
||||
$authorization = $connectionContainer->get('authorization');
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
@@ -656,13 +649,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$timelimit = $connectionContainer->get('timelimit');
|
||||
$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);
|
||||
@@ -702,7 +697,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
* Skip this check for non-web platforms which are not required to send an origin header.
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
$originValidator = $connectionContainer->get('originValidator');
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
@@ -712,11 +707,43 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
|
||||
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($projectId, [
|
||||
'projectId' => $projectId,
|
||||
'teamId' => $teamId
|
||||
]);
|
||||
$stats->incr($projectId, 'connections');
|
||||
$stats->incr($projectId, 'connectionsTotal');
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_CONNECTIONS => 1,
|
||||
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
|
||||
], $projectId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Channels Check
|
||||
*/
|
||||
if (empty($channels)) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
|
||||
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
|
||||
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$connectedPayloadJson = json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => [],
|
||||
'subscriptions' => [],
|
||||
'user' => $sanitizedUser
|
||||
]
|
||||
]);
|
||||
|
||||
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
return;
|
||||
}
|
||||
|
||||
$names = array_keys($channels);
|
||||
@@ -740,7 +767,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'],
|
||||
$subscription['queries']
|
||||
$subscription['queries'],
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
$mapping[$index] = $subscriptionId;
|
||||
@@ -760,20 +788,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($project->getId(), [
|
||||
'projectId' => $project->getId(),
|
||||
'teamId' => $project->getAttribute('teamId')
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
|
||||
$connectedOutboundBytes = \strlen($connectedPayloadJson);
|
||||
|
||||
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
|
||||
|
||||
} catch (Throwable $th) {
|
||||
@@ -789,7 +804,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
@@ -804,7 +819,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$server->send([$connection], json_encode($response));
|
||||
$server->close($connection, $code);
|
||||
|
||||
if (Http::isDevelopment()) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development') {
|
||||
Console::error('[Error] Connection Error');
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
@@ -815,7 +830,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -941,7 +955,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$queries
|
||||
$queries,
|
||||
$user->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -975,6 +990,99 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
break;
|
||||
|
||||
case 'subscribe':
|
||||
/**
|
||||
* Message based upsertion of a subscription
|
||||
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
|
||||
* If non-existing subid is given or not given a new subid will be generated
|
||||
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
|
||||
*
|
||||
* structure of the payload -> array of maps
|
||||
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
|
||||
*/
|
||||
if (!is_array($message['data']) || !array_is_list($message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
|
||||
$userId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
// bulk validation + parsing before subscribing
|
||||
$parsedPayloads = [];
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (!\is_array($payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
|
||||
}
|
||||
if (!array_key_exists('channels', $payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
|
||||
}
|
||||
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
|
||||
}
|
||||
// registering the queries if not present and check in the same payload later on
|
||||
if (!array_key_exists('queries', $payload)) {
|
||||
$payload['queries'] = [];
|
||||
}
|
||||
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
|
||||
}
|
||||
|
||||
$subscriptionId = \array_key_exists('subscriptionId', $payload)
|
||||
? $payload['subscriptionId']
|
||||
: ID::unique();
|
||||
|
||||
try {
|
||||
$convertedQueries = Realtime::convertQueries($payload['queries']);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$parsedPayloads[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'channels' => $payload['channels'],
|
||||
'queries' => $convertedQueries,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($parsedPayloads as $parsedPayload) {
|
||||
$subscriptionId = $parsedPayload['subscriptionId'];
|
||||
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
|
||||
$queries = $parsedPayload['queries'];
|
||||
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
|
||||
}
|
||||
|
||||
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
|
||||
$responsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'subscribe',
|
||||
'success' => true,
|
||||
'subscriptions' => \array_map(function (array $parsedPayload) {
|
||||
return [
|
||||
'subscriptionId' => $parsedPayload['subscriptionId'],
|
||||
'channels' => $parsedPayload['channels'],
|
||||
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
|
||||
];
|
||||
}, $parsedPayloads),
|
||||
]
|
||||
]);
|
||||
|
||||
$server->send([$connection], $responsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$subscribeOutboundBytes = \strlen($responsePayload);
|
||||
|
||||
if ($subscribeOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
@@ -988,7 +1096,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
|
||||
@@ -993,7 +993,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.7.22
|
||||
image: openruntimes/executor:0.11.4
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
|
||||
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Swoole\Runtime;
|
||||
|
||||
Generated
+71
-71
@@ -1996,16 +1996,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.50",
|
||||
"version": "3.0.51",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2086,7 +2086,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2102,7 +2102,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-19T02:57:58+00:00"
|
||||
"time": "2026-04-10T01:33:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
@@ -2887,16 +2887,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2968,20 +2968,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-12-23T08:48:59+00:00"
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692"
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3028,7 +3028,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3048,20 +3048,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
|
||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3108,7 +3108,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3128,20 +3128,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-08T02:45:35+00:00"
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php85",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php85.git",
|
||||
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
|
||||
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
|
||||
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
|
||||
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3188,7 +3188,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3208,7 +3208,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-23T16:12:55+00:00"
|
||||
"time": "2026-04-10T16:50:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.19",
|
||||
"version": "5.3.20",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
|
||||
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
|
||||
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.19"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.20"
|
||||
},
|
||||
"time": "2026-03-31T15:52:08+00:00"
|
||||
"time": "2026-04-10T08:27:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4271,16 +4271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.19",
|
||||
"version": "0.34.20",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c"
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/995c119f31866cacd42d63b1f922bf86eabb396c",
|
||||
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4319,9 +4319,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.19"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.20"
|
||||
},
|
||||
"time": "2026-04-08T10:23:17+00:00"
|
||||
"time": "2026-04-12T14:25:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -5225,16 +5225,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
|
||||
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3",
|
||||
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5268,9 +5268,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/3.2.0"
|
||||
},
|
||||
"time": "2026-03-24T08:49:14+00:00"
|
||||
"time": "2026-04-08T16:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5448,16 +5448,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.17.7",
|
||||
"version": "1.17.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
|
||||
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5493,9 +5493,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.7"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.11"
|
||||
},
|
||||
"time": "2026-04-08T08:51:05+00:00"
|
||||
"time": "2026-04-11T02:42:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -7764,16 +7764,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7823,7 +7823,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7843,20 +7843,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
|
||||
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
|
||||
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7905,7 +7905,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7925,11 +7925,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-27T09:58:17+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -7990,7 +7990,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8014,7 +8014,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -8070,7 +8070,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8449,5 +8449,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Appwrite\Extend\TraceFunctionExecution;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
@@ -24,16 +25,27 @@ class Log extends Listener
|
||||
{
|
||||
$this
|
||||
->desc('Persists execution logs to database via queue')
|
||||
->inject('publisher')
|
||||
->inject('publisherForExecutions')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(ExecutionCompleted $event, Publisher $publisher): void
|
||||
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
|
||||
{
|
||||
$queueForExecutions = new Execution($publisher);
|
||||
$queueForExecutions
|
||||
->setExecution(new Document($event->execution))
|
||||
->setProject(new Document($event->project))
|
||||
->trigger();
|
||||
$project = new Document($event->project);
|
||||
$execution = new Document($event->execution);
|
||||
if ($execution->getAttribute('resourceType', '') === 'functions') {
|
||||
TraceFunctionExecution::log('v1_executions_enqueue', [
|
||||
'projectId' => $project->getId(),
|
||||
'functionId' => $execution->getAttribute('resourceId', ''),
|
||||
'executionId' => $execution->getId(),
|
||||
'deploymentId' => $execution->getAttribute('deploymentId', ''),
|
||||
'status' => $execution->getAttribute('status', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
$publisherForExecutions->enqueue(new ExecutionMessage(
|
||||
project: $project,
|
||||
execution: $execution,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class Mails extends Listener
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setProject($project)
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Execution extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly Document $execution,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
'execution' => $this->execution->getArrayCopy(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
execution: new Document($data['execution'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Migration extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly Document $migration,
|
||||
public readonly array $platform = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
'migration' => $this->migration->getArrayCopy(),
|
||||
'platform' => $this->platform,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
migration: new Document($data['migration'] ?? []),
|
||||
platform: $data['platform'] ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class StatsResources extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Execution extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(ExecutionMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Migration as MigrationMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Migration extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(MigrationMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
|
||||
use Utopia\Console;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class StatsResources extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(StatsResourcesMessage $message): string|bool
|
||||
{
|
||||
// Resource stats are best-effort; publishing failures should not interrupt the scheduler loop.
|
||||
try {
|
||||
return $this->publish($this->queue, $message);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[StatsResources] Failed to publish stats resources message: ' . $th->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Extend;
|
||||
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
* Opt-in trace logging for a single project+function pair via env (development / debugging).
|
||||
* Set both _APP_TRACE_PROJECT_ID and _APP_TRACE_FUNCTION_ID to enable.
|
||||
*/
|
||||
class TraceFunctionExecution
|
||||
{
|
||||
private static ?string $cachedProjectId = null;
|
||||
|
||||
private static ?string $cachedFunctionId = null;
|
||||
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::traceProjectId() !== '' && self::traceFunctionId() !== '';
|
||||
}
|
||||
|
||||
private static function traceProjectId(): string
|
||||
{
|
||||
if (self::$cachedProjectId === null) {
|
||||
self::$cachedProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '69ddf60f001255461d5c');
|
||||
}
|
||||
|
||||
return self::$cachedProjectId;
|
||||
}
|
||||
|
||||
private static function traceFunctionId(): string
|
||||
{
|
||||
if (self::$cachedFunctionId === null) {
|
||||
self::$cachedFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '69ddf6500032ee9f5b0f');
|
||||
}
|
||||
|
||||
return self::$cachedFunctionId;
|
||||
}
|
||||
|
||||
public static function matches(?string $projectId, ?string $functionId): bool
|
||||
{
|
||||
if (!self::isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $projectId === self::traceProjectId()
|
||||
&& (string) $functionId === self::traceFunctionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context Must include projectId and functionId for filtering.
|
||||
*/
|
||||
public static function log(string $station, array $context = []): void
|
||||
{
|
||||
if (!self::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$projectId = $context['projectId'] ?? null;
|
||||
$functionId = $context['functionId'] ?? null;
|
||||
if (!self::matches($projectId, $functionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = \array_merge([
|
||||
'station' => $station,
|
||||
'time' => DateTime::now(),
|
||||
], $context);
|
||||
|
||||
Console::log('[execution-trace] ' . \json_encode($payload, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
@@ -262,11 +262,12 @@ class Resolvers
|
||||
$request = clone $request;
|
||||
$utopia->setResource('request', static fn () => $request);
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
$response->clearSent();
|
||||
|
||||
try {
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
|
||||
$utopia->execute($route, $request);
|
||||
$utopia->execute($route, $request, $response);
|
||||
} catch (\Throwable $e) {
|
||||
if ($beforeReject) {
|
||||
$e = $beforeReject($e);
|
||||
|
||||
@@ -20,6 +20,7 @@ class Realtime extends MessagingAdapter
|
||||
* [CONNECTION_ID] ->
|
||||
* 'projectId' -> [PROJECT_ID]
|
||||
* 'roles' -> [ROLE_x, ROLE_Y]
|
||||
* 'userId' -> [USER_ID]
|
||||
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
|
||||
*/
|
||||
public array $connections = [];
|
||||
@@ -67,25 +68,35 @@ class Realtime extends MessagingAdapter
|
||||
* @param array $queryGroup Array of Query objects for this subscription (AND logic within subscription)
|
||||
* @return void
|
||||
*/
|
||||
public function subscribe(string $projectId, mixed $identifier, string $subscriptionId, array $roles, array $channels, array $queryGroup = []): void
|
||||
{
|
||||
public function subscribe(
|
||||
string $projectId,
|
||||
mixed $identifier,
|
||||
string $subscriptionId,
|
||||
array $roles,
|
||||
array $channels,
|
||||
array $queryGroup = [],
|
||||
?string $userId = null
|
||||
): void {
|
||||
if (!isset($this->subscriptions[$projectId])) { // Init Project
|
||||
$this->subscriptions[$projectId] = [];
|
||||
}
|
||||
|
||||
$strings = [];
|
||||
if (empty($queryGroup)) {
|
||||
$strings[] = Query::select(['*'])->toString();
|
||||
} else {
|
||||
foreach ($queryGroup as $query) {
|
||||
$strings[] = $query->toString();
|
||||
}
|
||||
}
|
||||
$data = [];
|
||||
|
||||
$data = [
|
||||
'strings' => $strings,
|
||||
'compiled' => RuntimeQuery::compile($queryGroup),
|
||||
];
|
||||
if (!empty($channels)) {
|
||||
if (empty($queryGroup)) {
|
||||
$strings[] = Query::select(['*'])->toString();
|
||||
} else {
|
||||
foreach ($queryGroup as $query) {
|
||||
$strings[] = $query->toString();
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'strings' => $strings,
|
||||
'compiled' => RuntimeQuery::compile($queryGroup),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if (!isset($this->subscriptions[$projectId][$role])) {
|
||||
@@ -103,10 +114,12 @@ class Realtime extends MessagingAdapter
|
||||
}
|
||||
}
|
||||
|
||||
// Update connection info
|
||||
// Keep userId from onOpen/authentication when provided.
|
||||
// Fallback to existing stored value for subsequent subscribe upserts.
|
||||
$this->connections[$identifier] = [
|
||||
'projectId' => $projectId,
|
||||
'roles' => $roles,
|
||||
'userId' => $userId ?? ($this->connections[$identifier]['userId'] ?? ''),
|
||||
'channels' => $channels
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ abstract class Migration
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+18
-5
@@ -145,7 +145,11 @@ class XList extends Action
|
||||
$documentsField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_DOCUMENTS);
|
||||
|
||||
$documentsCacheHit = false;
|
||||
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
|
||||
try {
|
||||
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
|
||||
} catch (\Throwable) {
|
||||
$cachedDocuments = null;
|
||||
}
|
||||
|
||||
if ($cachedDocuments !== null &&
|
||||
$cachedDocuments !== false &&
|
||||
@@ -157,21 +161,30 @@ class XList extends Action
|
||||
} else {
|
||||
$documents = $find();
|
||||
|
||||
// Convert Document objects to arrays for caching
|
||||
$documentsArray = \array_map(function ($doc) {
|
||||
return $doc->getArrayCopy();
|
||||
}, $documents);
|
||||
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
|
||||
try {
|
||||
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($includeTotal) {
|
||||
$totalField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_TOTAL);
|
||||
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
|
||||
try {
|
||||
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
|
||||
} catch (\Throwable) {
|
||||
$cachedTotal = null;
|
||||
}
|
||||
if ($cachedTotal !== null && $cachedTotal !== false) {
|
||||
$total = $cachedTotal;
|
||||
} else {
|
||||
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
|
||||
try {
|
||||
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$total = 0;
|
||||
|
||||
@@ -305,9 +305,7 @@ class Create extends Base
|
||||
|
||||
if ($async) {
|
||||
if (is_null($scheduledAt)) {
|
||||
if ($project->getId() != '6862e6a6000cce69f9da') {
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
$queueForFunctions
|
||||
->setType('http')
|
||||
->setExecution($execution)
|
||||
@@ -348,9 +346,7 @@ class Create extends Base
|
||||
->setAttribute('scheduleInternalId', $schedule->getSequence())
|
||||
->setAttribute('scheduledAt', $scheduledAt);
|
||||
|
||||
if ($project->getId() != '6862e6a6000cce69f9da') {
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
@@ -516,9 +512,7 @@ class Create extends Base
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
;
|
||||
|
||||
if ($project->getId() != '6862e6a6000cce69f9da') {
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
|
||||
$executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId();
|
||||
|
||||
@@ -11,10 +11,10 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -78,13 +78,13 @@ class Get extends Base
|
||||
->inject('queueForAudits')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForStatsResources')
|
||||
->inject('publisherForStatsResources')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForCertificates')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->inject('queueForScreenshots')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
@@ -98,13 +98,13 @@ class Get extends Base
|
||||
Audit $queueForAudits,
|
||||
Mail $queueForMails,
|
||||
Func $queueForFunctions,
|
||||
StatsResources $queueForStatsResources,
|
||||
StatsResourcesPublisher $publisherForStatsResources,
|
||||
UsagePublisher $publisherForUsage,
|
||||
Webhook $queueForWebhooks,
|
||||
Certificate $queueForCertificates,
|
||||
Build $queueForBuilds,
|
||||
Messaging $queueForMessaging,
|
||||
Migration $queueForMigrations,
|
||||
MigrationPublisher $publisherForMigrations,
|
||||
Screenshot $queueForScreenshots,
|
||||
): void {
|
||||
$threshold = (int) $threshold;
|
||||
@@ -115,14 +115,14 @@ class Get extends Base
|
||||
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits,
|
||||
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
|
||||
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
|
||||
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $queueForStatsResources,
|
||||
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
|
||||
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
|
||||
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
|
||||
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates,
|
||||
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
|
||||
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
|
||||
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
|
||||
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations,
|
||||
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
|
||||
};
|
||||
$failed = $queue->getSize(failed: true);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Migrations;
|
||||
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -42,16 +42,16 @@ class Get extends Base
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int|string $threshold, Migration $queueForMigrations, Response $response): void
|
||||
public function action(int|string $threshold, MigrationPublisher $publisherForMigrations, Response $response): void
|
||||
{
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$size = $queueForMigrations->getSize();
|
||||
$size = $publisherForMigrations->getSize();
|
||||
|
||||
$this->assertQueueThreshold($size, $threshold);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsResources;
|
||||
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -42,16 +42,16 @@ class Get extends Base
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('queueForStatsResources')
|
||||
->inject('publisherForStatsResources')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int|string $threshold, StatsResources $queueForStatsResources, Response $response): void
|
||||
public function action(int|string $threshold, StatsResourcesPublisher $publisherForStatsResources, Response $response): void
|
||||
{
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$size = $queueForStatsResources->getSize();
|
||||
$size = $publisherForStatsResources->getSize();
|
||||
|
||||
$this->assertQueueThreshold($size, $threshold);
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Create extends Base
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
|
||||
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
@@ -72,13 +72,10 @@ class Create extends Base
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -95,7 +92,7 @@ class Create extends Base
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'scopes' => $scopes,
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
|
||||
@@ -60,7 +60,7 @@ class Update extends Base
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
|
||||
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
@@ -70,13 +70,10 @@ class Update extends Base
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -92,7 +89,7 @@ class Update extends Base
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'scopes' => $scopes,
|
||||
'expire' => $expire,
|
||||
]);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class Create extends Action
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
|
||||
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->inject('request')
|
||||
|
||||
@@ -56,7 +56,7 @@ class Update extends Action
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
|
||||
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
|
||||
@@ -0,0 +1,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());
|
||||
|
||||
@@ -15,7 +15,6 @@ use Utopia\Compression\Algorithms\GZIP;
|
||||
use Utopia\Compression\Algorithms\Zstd;
|
||||
use Utopia\Compression\Compression;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
@@ -26,6 +25,7 @@ use Utopia\Http\Adapter\Swoole\Request;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\HexColor;
|
||||
@@ -269,7 +269,17 @@ class Get extends Action
|
||||
|
||||
$totalTime = \microtime(true) - $startTime;
|
||||
|
||||
Console::info("File preview rendered,project=" . $project->getId() . ",bucket=" . $bucketId . ",file=" . $file->getId() . ",uri=" . $request->getURI() . ",total=" . $totalTime . ",rendering=" . $renderingTime . ",decryption=" . $decryptionTime . ",decompression=" . $decompressionTime . ",download=" . $downloadTime);
|
||||
Span::add('storage.file.id', $file->getId());
|
||||
Span::add('storage.bucket.id', $bucketId);
|
||||
Span::add('storage.file.size_bytes', $file->getAttribute('sizeActual'));
|
||||
if (!empty($type)) {
|
||||
Span::add('storage.file.extension', $type);
|
||||
}
|
||||
Span::add('storage.timing.download_seconds', $downloadTime);
|
||||
Span::add('storage.timing.decryption_seconds', $decryptionTime);
|
||||
Span::add('storage.timing.decompression_seconds', $decompressionTime);
|
||||
Span::add('storage.timing.rendering_seconds', $renderingTime);
|
||||
Span::add('storage.timing.total_seconds', $totalTime);
|
||||
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ 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)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
@@ -85,9 +85,9 @@ class Create extends Action
|
||||
string $name,
|
||||
array $events,
|
||||
bool $enabled,
|
||||
bool $security,
|
||||
string $httpUser,
|
||||
string $httpPass,
|
||||
bool $tls,
|
||||
string $authUsername,
|
||||
string $authPassword,
|
||||
Response $response,
|
||||
Document $project,
|
||||
QueueEvent $queueForEvents,
|
||||
@@ -104,9 +104,9 @@ class Create extends Action
|
||||
'name' => $name,
|
||||
'events' => $events,
|
||||
'url' => $url,
|
||||
'security' => $security,
|
||||
'httpUser' => $httpUser,
|
||||
'httpPass' => $httpPass,
|
||||
'security' => $tls,
|
||||
'httpUser' => $authUsername,
|
||||
'httpPass' => $authPassword,
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
@@ -28,9 +28,9 @@ class Update extends Action
|
||||
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 +39,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: [
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Extend\TraceFunctionExecution;
|
||||
use Cron\CronExpression;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -97,6 +98,16 @@ class ScheduleFunctions extends ScheduleBase
|
||||
|
||||
$queueForFunctions = new Func($this->publisherFunctions);
|
||||
|
||||
$projectDoc = $schedule['project'];
|
||||
$functionDoc = $schedule['resource'];
|
||||
TraceFunctionExecution::log('v1_functions_enqueue', [
|
||||
'projectId' => $projectDoc->getId(),
|
||||
'functionId' => $functionDoc->getId(),
|
||||
'scheduleId' => $schedule['$id'] ?? '',
|
||||
'schedule' => $schedule['schedule'] ?? '',
|
||||
]);
|
||||
|
||||
|
||||
$queueForFunctions
|
||||
->setType('schedule')
|
||||
->setFunction($schedule['resource'])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\StatsResources as EventStatsResources;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Platform\Action;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -43,11 +43,11 @@ class StatsResources extends Action
|
||||
->desc('Schedules projects for usage count')
|
||||
->inject('dbForPlatform')
|
||||
->inject('logError')
|
||||
->inject('queueForStatsResources')
|
||||
->inject('publisherForStatsResources')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(Database $dbForPlatform, callable $logError, EventStatsResources $queueForStatsResources): void
|
||||
public function action(Database $dbForPlatform, callable $logError, StatsResourcesPublisher $publisherForStatsResources): void
|
||||
{
|
||||
$this->logError = $logError;
|
||||
$this->dbForPlatform = $dbForPlatform;
|
||||
@@ -60,7 +60,7 @@ class StatsResources extends Action
|
||||
|
||||
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
|
||||
|
||||
Console::loop(function () use ($queueForStatsResources) {
|
||||
Console::loop(function () use ($publisherForStatsResources) {
|
||||
|
||||
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
|
||||
/**
|
||||
@@ -69,10 +69,10 @@ class StatsResources extends Action
|
||||
$this->foreachDocument($this->dbForPlatform, 'projects', [
|
||||
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours)),
|
||||
Query::equal('region', [System::getEnv('_APP_REGION', 'default')])
|
||||
], function ($project) use ($queueForStatsResources) {
|
||||
$queueForStatsResources
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
], function ($project) use ($publisherForStatsResources) {
|
||||
$publisherForStatsResources->enqueue(new \Appwrite\Event\Message\StatsResources(
|
||||
project: $project,
|
||||
));
|
||||
Console::success('project: ' . $project->getId() . '(' . $project->getSequence() . ')' . ' queued');
|
||||
});
|
||||
}, $interval);
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Event\Message\Execution;
|
||||
use Appwrite\Extend\TraceFunctionExecution;
|
||||
use Exception;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
|
||||
@@ -32,21 +33,21 @@ class Executions extends Action
|
||||
Message $message,
|
||||
Database $dbForProject,
|
||||
): void {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$execution = new Document($payload['execution'] ?? []);
|
||||
$executionMessage = Execution::fromArray($message->getPayload() ?? []);
|
||||
$execution = $executionMessage->execution;
|
||||
|
||||
if ($execution->isEmpty()) {
|
||||
throw new Exception('Missing execution');
|
||||
}
|
||||
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
if ($project->getId() != '6862e6a6000cce69f9da') {
|
||||
$dbForProject->upsertDocument('executions', $execution);
|
||||
}
|
||||
TraceFunctionExecution::log('executions_worker_upsert', [
|
||||
'projectId' => $executionMessage->project->getId(),
|
||||
'functionId' => $execution->getAttribute('resourceId', ''),
|
||||
'executionId' => $execution->getId(),
|
||||
'deploymentId' => $execution->getAttribute('deploymentId', ''),
|
||||
'resourceType' => $execution->getAttribute('resourceType', ''),
|
||||
]);
|
||||
|
||||
$dbForProject->upsertDocument('executions', $execution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Extend\TraceFunctionExecution;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Executor\Executor;
|
||||
use Utopia\Bus\Bus;
|
||||
@@ -115,6 +116,17 @@ class Functions extends Action
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('type', $type);
|
||||
|
||||
if (empty($events) && !$function->isEmpty()) {
|
||||
TraceFunctionExecution::log('functions_worker_dequeue', [
|
||||
'projectId' => $project->getId(),
|
||||
'functionId' => $function->getId(),
|
||||
'payloadType' => $type,
|
||||
'queuePid' => $message->getPid(),
|
||||
'queueName' => $message->getQueue(),
|
||||
'messageTimestamp' => $message->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($events)) {
|
||||
$limit = 100;
|
||||
$sum = 100;
|
||||
@@ -304,6 +316,15 @@ class Functions extends Action
|
||||
'duration' => 0.0,
|
||||
]);
|
||||
|
||||
TraceFunctionExecution::log('functions_worker_before_execution_completed_bus_fail', [
|
||||
'projectId' => $project->getId(),
|
||||
'functionId' => $function->getId(),
|
||||
'executionId' => $execution->getId(),
|
||||
'deploymentId' => $execution->getAttribute('deploymentId', ''),
|
||||
'trigger' => $trigger,
|
||||
'status' => $execution->getAttribute('status', ''),
|
||||
]);
|
||||
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
@@ -522,6 +543,13 @@ 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\"";
|
||||
TraceFunctionExecution::log('functions_worker_before_executor', [
|
||||
'projectId' => $project->getId(),
|
||||
'functionId' => $functionId,
|
||||
'executionId' => $executionId,
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => $trigger,
|
||||
]);
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deploymentId,
|
||||
@@ -594,6 +622,14 @@ class Functions extends Action
|
||||
$errorCode = $th->getCode();
|
||||
} finally {
|
||||
/** Persist final execution status and record usage */
|
||||
TraceFunctionExecution::log('functions_worker_before_execution_completed_bus', [
|
||||
'projectId' => $project->getId(),
|
||||
'functionId' => $functionId,
|
||||
'executionId' => $execution->getId(),
|
||||
'deploymentId' => $execution->getAttribute('deploymentId', ''),
|
||||
'status' => $execution->getAttribute('status', ''),
|
||||
'trigger' => $trigger,
|
||||
]);
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Message\Migration;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
@@ -129,7 +130,7 @@ class Migrations extends Action
|
||||
array $plan,
|
||||
Authorization $authorization,
|
||||
): void {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$migrationMessage = Migration::fromArray($message->getPayload() ?? []);
|
||||
$this->getDatabasesDB = $getDatabasesDB;
|
||||
$this->getProjectDB = $getProjectDB;
|
||||
|
||||
@@ -137,12 +138,7 @@ class Migrations extends Action
|
||||
$this->deviceForFiles = $deviceForFiles;
|
||||
$this->plan = $plan;
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$events = $payload['events'] ?? [];
|
||||
$migration = new Document($payload['migration'] ?? []);
|
||||
$migration = $migrationMessage->migration;
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new \Exception('Migration not found');
|
||||
@@ -161,11 +157,7 @@ class Migrations extends Action
|
||||
$this->project = $project;
|
||||
$this->logError = $logError;
|
||||
|
||||
$platform = $payload['platform'] ?? Config::getParam('platform', []);
|
||||
|
||||
if (!empty($events)) {
|
||||
return;
|
||||
}
|
||||
$platform = $migrationMessage->platform ?: Config::getParam('platform', []);
|
||||
|
||||
try {
|
||||
$this->processMigration(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
|
||||
use Appwrite\Platform\Action;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
@@ -67,8 +68,8 @@ class StatsResources extends Action
|
||||
{
|
||||
$this->logError = $logError;
|
||||
|
||||
$payload = $message->getPayload() ?? [];
|
||||
if (empty($payload)) {
|
||||
$statsResources = StatsResourcesMessage::fromArray($message->getPayload() ?? []);
|
||||
if ($statsResources->project->isEmpty()) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,18 @@ abstract class Format
|
||||
],
|
||||
];
|
||||
|
||||
private const array REQUEST_PARAMETER_OVERRIDES = [
|
||||
[
|
||||
'namespace' => 'project',
|
||||
'methods' => [
|
||||
'createWebPlatform',
|
||||
'updateWebPlatform',
|
||||
],
|
||||
'parameter' => 'hostname',
|
||||
'required' => true,
|
||||
],
|
||||
];
|
||||
|
||||
protected array $enumBlacklist = [];
|
||||
|
||||
public function __construct(Container $container, array $services, array $routes, array $models, array $keys, int $authCount, string $platform)
|
||||
@@ -774,8 +786,38 @@ abstract class Format
|
||||
return $values;
|
||||
}
|
||||
|
||||
protected function getRequestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array
|
||||
{
|
||||
$config = [
|
||||
'required' => !$optional,
|
||||
'nullable' => $nullable,
|
||||
];
|
||||
|
||||
foreach (self::REQUEST_PARAMETER_OVERRIDES as $override) {
|
||||
if (
|
||||
$override['namespace'] !== $service
|
||||
|| !\in_array($method, $override['methods'], true)
|
||||
|| $override['parameter'] !== $param
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config['required'] = $override['required'] ?? $config['required'];
|
||||
$config['nullable'] = $override['nullable'] ?? $config['nullable'];
|
||||
break;
|
||||
}
|
||||
|
||||
$config['emitDefault'] = !$config['required'] && !\is_null($default);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function getResponseEnumName(string $model, string $param): ?string
|
||||
{
|
||||
if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') {
|
||||
return 'PlatformType';
|
||||
}
|
||||
|
||||
if ($param !== 'status') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -381,14 +381,23 @@ class OpenAPI3 extends Format
|
||||
*/
|
||||
$validator = $this->getValidator($param);
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
$parameter = $this->getRequestParameterConfig(
|
||||
$sdk->getNamespace() ?? '',
|
||||
$methodName,
|
||||
$name,
|
||||
$param['optional'],
|
||||
$isNullable,
|
||||
$param['default'],
|
||||
);
|
||||
|
||||
$node = [
|
||||
'name' => $name,
|
||||
'description' => $param['description'],
|
||||
'required' => !$param['optional'],
|
||||
'required' => $parameter['required'],
|
||||
];
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
if ($isNullable) {
|
||||
/** @var Nullable $validator */
|
||||
$validator = $validator->getValidator();
|
||||
@@ -735,7 +744,7 @@ class OpenAPI3 extends Format
|
||||
break;
|
||||
}
|
||||
|
||||
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
|
||||
if ($parameter['emitDefault']) { // Param has default value
|
||||
$node['schema']['default'] = $param['default'];
|
||||
}
|
||||
|
||||
@@ -746,7 +755,7 @@ class OpenAPI3 extends Format
|
||||
$node['in'] = 'query';
|
||||
$temp['parameters'][] = $node;
|
||||
} else { // Param is in payload
|
||||
if (!$param['optional']) {
|
||||
if ($node['required']) {
|
||||
$bodyRequired[] = $name;
|
||||
}
|
||||
|
||||
@@ -783,7 +792,7 @@ class OpenAPI3 extends Format
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
|
||||
if ($isNullable) {
|
||||
if ($parameter['nullable']) {
|
||||
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,14 +383,23 @@ class Swagger2 extends Format
|
||||
/** @var Validator $validator */
|
||||
$validator = $this->getValidator($param);
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
$parameter = $this->getRequestParameterConfig(
|
||||
$sdk->getNamespace() ?? '',
|
||||
$methodName,
|
||||
$name,
|
||||
$param['optional'],
|
||||
$isNullable,
|
||||
$param['default'],
|
||||
);
|
||||
|
||||
$node = [
|
||||
'name' => $name,
|
||||
'description' => $param['description'],
|
||||
'required' => !$param['optional'],
|
||||
'required' => $parameter['required'],
|
||||
];
|
||||
|
||||
$isNullable = $validator instanceof Nullable;
|
||||
|
||||
if ($isNullable) {
|
||||
/** @var Nullable $validator */
|
||||
$validator = $validator->getValidator();
|
||||
@@ -711,7 +720,7 @@ class Swagger2 extends Format
|
||||
break;
|
||||
}
|
||||
|
||||
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
|
||||
if ($parameter['emitDefault']) { // Param has default value
|
||||
$node['default'] = $param['default'];
|
||||
}
|
||||
|
||||
@@ -729,7 +738,7 @@ class Swagger2 extends Format
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$param['optional']) {
|
||||
if ($node['required']) {
|
||||
$bodyRequired[] = $name;
|
||||
}
|
||||
|
||||
@@ -755,7 +764,7 @@ class Swagger2 extends Format
|
||||
$body['schema']['properties'][$name]['x-global'] = true;
|
||||
}
|
||||
|
||||
if ($isNullable) {
|
||||
if ($parameter['nullable']) {
|
||||
$body['schema']['properties'][$name]['x-nullable'] = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,28 @@ namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
class Webhooks extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'name',
|
||||
'url',
|
||||
'authUsername',
|
||||
'tls',
|
||||
'events',
|
||||
'enabled',
|
||||
'logs',
|
||||
'attempts',
|
||||
];
|
||||
|
||||
/**
|
||||
* Map API attribute names to DB column names.
|
||||
*/
|
||||
private const ATTRIBUTE_ALIASES = [
|
||||
'tls' => 'security',
|
||||
'authUsername' => 'httpUser',
|
||||
];
|
||||
|
||||
/**
|
||||
* DB column names used for schema validation.
|
||||
*/
|
||||
private const DB_ATTRIBUTES = [
|
||||
'name',
|
||||
'url',
|
||||
'httpUser',
|
||||
@@ -21,6 +43,26 @@ class Webhooks extends Base
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('webhooks', self::ALLOWED_ATTRIBUTES);
|
||||
parent::__construct('webhooks', self::DB_ATTRIBUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert API attribute names to DB column names in query strings before validation.
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (\is_array($value)) {
|
||||
foreach ($value as &$queryString) {
|
||||
if (!\is_string($queryString)) {
|
||||
continue;
|
||||
}
|
||||
foreach (self::ATTRIBUTE_ALIASES as $alias => $dbName) {
|
||||
$queryString = \str_replace('"' . $alias . '"', '"' . $dbName . '"', $queryString);
|
||||
}
|
||||
}
|
||||
unset($queryString);
|
||||
}
|
||||
|
||||
return parent::isValid($value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
|
||||
class V22 extends Filter
|
||||
{
|
||||
// Convert 1.9.0 params to 1.9.1
|
||||
protected function parseUpdateProtocolStatus(array $content): array
|
||||
{
|
||||
if (isset($content['api'])) {
|
||||
$content['protocolId'] = $content['api'];
|
||||
unset($content['api']);
|
||||
}
|
||||
|
||||
if (isset($content['status'])) {
|
||||
$content['enabled'] = $content['status'];
|
||||
unset($content['status']);
|
||||
}
|
||||
|
||||
if (($content['protocolId'] ?? '') === 'realtime') {
|
||||
$content['protocolId'] = 'websocket';
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseUpdateServiceStatus(array $content): array
|
||||
{
|
||||
if (isset($content['service'])) {
|
||||
$content['serviceId'] = $content['service'];
|
||||
unset($content['service']);
|
||||
}
|
||||
|
||||
if (isset($content['status'])) {
|
||||
$content['enabled'] = $content['status'];
|
||||
unset($content['status']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseKeyScopes(array $content): array
|
||||
{
|
||||
if (!\is_array($content['scopes'] ?? null)) {
|
||||
$content['scopes'] = [];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseWebhook(array $content): array
|
||||
{
|
||||
if (isset($content['security'])) {
|
||||
$content['tls'] = $content['security'];
|
||||
unset($content['security']);
|
||||
}
|
||||
|
||||
if (isset($content['httpUser'])) {
|
||||
$content['authUsername'] = $content['httpUser'];
|
||||
unset($content['httpUser']);
|
||||
}
|
||||
|
||||
if (isset($content['httpPass'])) {
|
||||
$content['authPassword'] = $content['httpPass'];
|
||||
unset($content['httpPass']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'project.updateServiceStatus':
|
||||
$content = $this->parseUpdateServiceStatus($content);
|
||||
break;
|
||||
case 'project.updateProtocolStatus':
|
||||
$content = $this->parseUpdateProtocolStatus($content);
|
||||
break;
|
||||
case 'project.createKey':
|
||||
case 'project.updateKey':
|
||||
$content = $this->parseKeyScopes($content);
|
||||
break;
|
||||
case 'webhooks.create':
|
||||
case 'webhooks.update':
|
||||
$content = $this->parseWebhook($content);
|
||||
break;
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -626,6 +626,17 @@ class Response extends SwooleResponse
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the sent flag so the response can be reused for another
|
||||
* action execution (e.g. batched GraphQL queries that share one
|
||||
* Response instance).
|
||||
*/
|
||||
public function clearSent(): static
|
||||
{
|
||||
$this->sent = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to add a response filter, the order of filters are first in - first out.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
|
||||
// Convert 1.9.1 Data format to 1.9.0 format
|
||||
class V22 extends Filter
|
||||
{
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
Response::MODEL_PROJECT => $this->parseProject($content),
|
||||
Response::MODEL_WEBHOOK => $this->parseWebhook($content),
|
||||
Response::MODEL_WEBHOOK_LIST => $this->handleList($content, 'webhooks', fn ($item) => $this->parseWebhook($item)),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
|
||||
private function parseProject(array $content): array
|
||||
{
|
||||
foreach (['protocolStatusForRest', 'protocolStatusForGraphql', 'protocolStatusForWebsocket'] as $field) {
|
||||
unset($content[$field]);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function parseWebhook(array $content): array
|
||||
{
|
||||
$content['security'] = $content['tls'] ?? true;
|
||||
unset($content['tls']);
|
||||
|
||||
$content['httpUser'] = $content['authUsername'] ?? '';
|
||||
unset($content['authUsername']);
|
||||
|
||||
$content['httpPass'] = $content['authPassword'] ?? '';
|
||||
unset($content['authPassword']);
|
||||
|
||||
$content['signatureKey'] = $content['secret'] ?? '';
|
||||
unset($content['secret']);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,22 @@ class Project extends Model
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
$apis = Config::getParam('protocols', []);
|
||||
|
||||
foreach ($apis as $api) {
|
||||
$name = $api['name'] ?? '';
|
||||
$key = $api['key'] ?? '';
|
||||
|
||||
$this
|
||||
->addRule('protocolStatusFor' . ucfirst($key), [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => $name . ' protocol status',
|
||||
'example' => true,
|
||||
'default' => true,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,6 +391,7 @@ class Project extends Model
|
||||
{
|
||||
$this->expandSmtpFields($document);
|
||||
$this->expandServiceFields($document);
|
||||
$this->expandApiFields($document);
|
||||
$this->expandAuthFields($document);
|
||||
$this->expandOAuthProviders($document);
|
||||
|
||||
@@ -419,6 +436,22 @@ class Project extends Model
|
||||
}
|
||||
}
|
||||
|
||||
private function expandApiFields(Document $document): void
|
||||
{
|
||||
if (!$document->isSet('apis')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values = $document->getAttribute('apis', []);
|
||||
$apis = Config::getParam('protocols', []);
|
||||
|
||||
foreach ($apis as $api) {
|
||||
$key = $api['key'] ?? '';
|
||||
$value = $values[$key] ?? true;
|
||||
$document->setAttribute('protocolStatusFor' . ucfirst($key), $value);
|
||||
}
|
||||
}
|
||||
|
||||
private function expandAuthFields(Document $document): void
|
||||
{
|
||||
if (!$document->isSet('auths')) {
|
||||
|
||||
@@ -66,8 +66,9 @@ class Rule extends Model
|
||||
])
|
||||
->addRule('deploymentResourceType', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'required' => false,
|
||||
'description' => 'Type of deployment. Possible values are "function", "site". Used if rule\'s type is "deployment".',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => 'function',
|
||||
'enum' => ['function', 'site'],
|
||||
])
|
||||
|
||||
@@ -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.',
|
||||
'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
|
||||
*
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -101,18 +101,14 @@ trait RealtimeBase
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testConnectionFailureMissingChannels(): void
|
||||
public function testConnectionSuccessMissingChannels(): void
|
||||
{
|
||||
$client = $this->getWebsocket([]);
|
||||
$payload = json_decode($client->receive(), true);
|
||||
|
||||
$this->assertArrayHasKey("type", $payload);
|
||||
$this->assertArrayHasKey("data", $payload);
|
||||
$this->assertEquals("error", $payload["type"]);
|
||||
$this->assertEquals(1008, $payload["data"]["code"]);
|
||||
$this->assertEquals("Missing channels", $payload["data"]["message"]);
|
||||
\usleep(250000); // 250ms
|
||||
$this->expectException(ConnectionException::class); // Check if server disconnected client
|
||||
$this->assertEquals("connected", $payload["type"]);
|
||||
$client->close();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,521 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Realtime;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use WebSocket\Client as WebSocketClient;
|
||||
use WebSocket\TimeoutException;
|
||||
|
||||
class RealtimeCustomClientQueryTestWithMessage extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use RealtimeQueryBase;
|
||||
|
||||
protected function supportForCheckConnectionStatus(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same signature as `RealtimeBase::getWebsocket()`, but:
|
||||
* - never sends queries in the URL (avoids URL length limits)
|
||||
* - once connected, sends channel/query data using a `type: "subscribe"` message
|
||||
*/
|
||||
private function getWebsocket(
|
||||
array $channels = [],
|
||||
array $headers = [],
|
||||
?string $projectId = null,
|
||||
?array $queries = null,
|
||||
int $timeout = 2
|
||||
): WebSocketClient {
|
||||
if ($projectId === null) {
|
||||
$projectId = $this->getProject()['$id'];
|
||||
}
|
||||
|
||||
$queryString = \http_build_query([
|
||||
'project' => $projectId,
|
||||
]);
|
||||
|
||||
$client = new WebSocketClient(
|
||||
'ws://appwrite.test/v1/realtime?' . $queryString,
|
||||
[
|
||||
'headers' => $headers,
|
||||
'timeout' => $timeout,
|
||||
]
|
||||
);
|
||||
$connected = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('connected', $connected['type'] ?? null);
|
||||
|
||||
if (empty($channels)) {
|
||||
return $client;
|
||||
}
|
||||
|
||||
if ($queries === []) {
|
||||
$queries = [Query::select(['*'])->toString()];
|
||||
}
|
||||
|
||||
$payload = [[
|
||||
'channels' => $channels,
|
||||
]];
|
||||
|
||||
if ($queries !== null) {
|
||||
$payload[0]['queries'] = $queries;
|
||||
}
|
||||
|
||||
$existingSubscriptions = $connected['data']['subscriptions'] ?? [];
|
||||
if (!empty($existingSubscriptions)) {
|
||||
$payload[0]['subscriptionId'] = $existingSubscriptions[\array_key_first($existingSubscriptions)];
|
||||
}
|
||||
|
||||
$client->send(\json_encode([
|
||||
'type' => 'subscribe',
|
||||
'data' => $payload,
|
||||
]));
|
||||
|
||||
$response = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('response', $response['type'] ?? null);
|
||||
$this->assertEquals('subscribe', $response['data']['to'] ?? null);
|
||||
$this->assertTrue($response['data']['success'] ?? false);
|
||||
$this->assertArrayHasKey('subscriptions', $response['data']);
|
||||
$this->assertIsArray($response['data']['subscriptions']);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects (URL has no per-channel queries), then sends a subscribe message with the given query strings.
|
||||
* Used to assert server rejects unsupported query methods the same way as URL-based subscriptions.
|
||||
*
|
||||
* @param array<int, string> $queryStrings
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function receiveSubscribeMessageResponse(
|
||||
array $channels,
|
||||
array $headers,
|
||||
array $queryStrings
|
||||
): array {
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$queryString = \http_build_query([
|
||||
'project' => $projectId,
|
||||
]);
|
||||
|
||||
$client = new WebSocketClient(
|
||||
'ws://appwrite.test/v1/realtime?' . $queryString,
|
||||
[
|
||||
'headers' => $headers,
|
||||
'timeout' => 2,
|
||||
]
|
||||
);
|
||||
$connected = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('connected', $connected['type'] ?? null);
|
||||
|
||||
$client->send(\json_encode([
|
||||
'type' => 'subscribe',
|
||||
'data' => [[
|
||||
'channels' => $channels,
|
||||
'queries' => $queryStrings,
|
||||
]],
|
||||
]));
|
||||
|
||||
$response = \json_decode($client->receive(), true);
|
||||
$client->close();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getWebsocketWithCustomQuery(array $queryParams, array $headers = [], int $timeout = 2): WebSocketClient
|
||||
{
|
||||
$queryString = \http_build_query($queryParams);
|
||||
|
||||
return new WebSocketClient(
|
||||
'ws://appwrite.test/v1/realtime?' . $queryString,
|
||||
[
|
||||
'headers' => $headers,
|
||||
'timeout' => $timeout,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $payloadEntries
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function sendSubscribeMessage(WebSocketClient $client, array $payloadEntries): array
|
||||
{
|
||||
$client->send(\json_encode([
|
||||
'type' => 'subscribe',
|
||||
'data' => $payloadEntries,
|
||||
]));
|
||||
$response = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('response', $response['type'] ?? null);
|
||||
$this->assertEquals('subscribe', $response['data']['to'] ?? null);
|
||||
$this->assertTrue($response['data']['success'] ?? false);
|
||||
$this->assertArrayHasKey('subscriptions', $response['data']);
|
||||
$this->assertIsArray($response['data']['subscriptions']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* subscriptionId: update with id from connected, create by omitting id, explicit new id,
|
||||
* duplicate id in one bulk (last wins), mixed bulk, idempotent repeat, empty queries → select-all.
|
||||
*/
|
||||
public function testSubscribeMessageUpsertCreateAndEdgeCases(): void
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
];
|
||||
|
||||
$queryString = \http_build_query([
|
||||
'project' => $projectId,
|
||||
]);
|
||||
$client = new WebSocketClient(
|
||||
'ws://appwrite.test/v1/realtime?' . $queryString,
|
||||
[
|
||||
'headers' => $headers,
|
||||
'timeout' => 30,
|
||||
]
|
||||
);
|
||||
$connected = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('connected', $connected['type'] ?? null);
|
||||
$initialResponse = $this->sendSubscribeMessage($client, [[
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::select(['*'])->toString()],
|
||||
]]);
|
||||
$initialSubscriptionId = $initialResponse['data']['subscriptions'][0]['subscriptionId'] ?? '';
|
||||
$this->assertNotEmpty($initialSubscriptionId);
|
||||
|
||||
$q1 = [Query::equal('status', ['q1'])->toString()];
|
||||
$r1 = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => $q1,
|
||||
]]);
|
||||
$this->assertCount(1, $r1['data']['subscriptions']);
|
||||
$this->assertSame($initialSubscriptionId, $r1['data']['subscriptions'][0]['subscriptionId']);
|
||||
$this->assertSame($q1, $r1['data']['subscriptions'][0]['queries']);
|
||||
|
||||
$q2 = [Query::equal('status', ['q2'])->toString()];
|
||||
$r2 = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => $q2,
|
||||
]]);
|
||||
$this->assertSame($initialSubscriptionId, $r2['data']['subscriptions'][0]['subscriptionId']);
|
||||
$this->assertSame($q2, $r2['data']['subscriptions'][0]['queries']);
|
||||
|
||||
$rOmit = $this->sendSubscribeMessage($client, [[
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::equal('status', ['omitted-slot'])->toString()],
|
||||
]]);
|
||||
$mintedId = $rOmit['data']['subscriptions'][0]['subscriptionId'];
|
||||
$this->assertNotSame($initialSubscriptionId, $mintedId);
|
||||
$this->assertNotEmpty($mintedId);
|
||||
|
||||
$explicitNewId = ID::unique();
|
||||
$qExplicit = [Query::equal('status', ['explicit'])->toString()];
|
||||
$rExplicit = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $explicitNewId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => $qExplicit,
|
||||
]]);
|
||||
$this->assertSame($explicitNewId, $rExplicit['data']['subscriptions'][0]['subscriptionId']);
|
||||
$this->assertSame($qExplicit, $rExplicit['data']['subscriptions'][0]['queries']);
|
||||
|
||||
$qFirst = [Query::equal('status', ['dup-a'])->toString()];
|
||||
$qSecond = [Query::equal('status', ['dup-b'])->toString()];
|
||||
$rDup = $this->sendSubscribeMessage($client, [
|
||||
[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => $qFirst,
|
||||
],
|
||||
[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => $qSecond,
|
||||
],
|
||||
]);
|
||||
$this->assertCount(2, $rDup['data']['subscriptions']);
|
||||
$this->assertSame($initialSubscriptionId, $rDup['data']['subscriptions'][0]['subscriptionId']);
|
||||
$this->assertSame($initialSubscriptionId, $rDup['data']['subscriptions'][1]['subscriptionId']);
|
||||
$this->assertSame($qSecond, $rDup['data']['subscriptions'][1]['queries']);
|
||||
|
||||
$rMixed = $this->sendSubscribeMessage($client, [
|
||||
[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::equal('status', ['mixed-update'])->toString()],
|
||||
],
|
||||
[
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::equal('status', ['mixed-new'])->toString()],
|
||||
],
|
||||
]);
|
||||
$this->assertCount(2, $rMixed['data']['subscriptions']);
|
||||
$this->assertSame($initialSubscriptionId, $rMixed['data']['subscriptions'][0]['subscriptionId']);
|
||||
$mixedSecondId = $rMixed['data']['subscriptions'][1]['subscriptionId'];
|
||||
$this->assertNotSame($initialSubscriptionId, $mixedSecondId);
|
||||
$this->assertNotEmpty($mixedSecondId);
|
||||
|
||||
$rSame = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::equal('status', ['idempotent'])->toString()],
|
||||
]]);
|
||||
$rSameAgain = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => [Query::equal('status', ['idempotent'])->toString()],
|
||||
]]);
|
||||
$this->assertSame($rSame['data']['subscriptions'][0]['queries'], $rSameAgain['data']['subscriptions'][0]['queries']);
|
||||
|
||||
$rEmpty = $this->sendSubscribeMessage($client, [[
|
||||
'subscriptionId' => $initialSubscriptionId,
|
||||
'channels' => ['documents'],
|
||||
'queries' => [],
|
||||
]]);
|
||||
$this->assertCount(1, $rEmpty['data']['subscriptions']);
|
||||
$this->assertSame($initialSubscriptionId, $rEmpty['data']['subscriptions'][0]['subscriptionId']);
|
||||
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testInvalidQueryShouldNotSubscribe(): void
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
];
|
||||
|
||||
// Test 1: Simple invalid query method (contains is not allowed)
|
||||
$response = $this->receiveSubscribeMessageResponse(['documents'], $headers, [
|
||||
Query::contains('status', ['active'])->toString(),
|
||||
]);
|
||||
$this->assertEquals('error', $response['type']);
|
||||
$this->assertStringContainsString('not supported in Realtime queries', $response['data']['message']);
|
||||
$this->assertStringContainsString('contains', $response['data']['message']);
|
||||
|
||||
// Test 2: Invalid query method in nested AND query
|
||||
$response = $this->receiveSubscribeMessageResponse(['documents'], $headers, [
|
||||
Query::and([
|
||||
Query::equal('status', ['active']),
|
||||
Query::search('name', 'test'),
|
||||
])->toString(),
|
||||
]);
|
||||
$this->assertEquals('error', $response['type']);
|
||||
$this->assertStringContainsString('not supported in Realtime queries', $response['data']['message']);
|
||||
$this->assertStringContainsString('search', $response['data']['message']);
|
||||
|
||||
// Test 3: Invalid query method in nested OR query
|
||||
$response = $this->receiveSubscribeMessageResponse(['documents'], $headers, [
|
||||
Query::or([
|
||||
Query::equal('status', ['active']),
|
||||
Query::between('score', 0, 100),
|
||||
])->toString(),
|
||||
]);
|
||||
$this->assertEquals('error', $response['type']);
|
||||
$this->assertStringContainsString('not supported in Realtime queries', $response['data']['message']);
|
||||
$this->assertStringContainsString('between', $response['data']['message']);
|
||||
|
||||
// Test 4: Deeply nested invalid query (AND -> OR -> invalid)
|
||||
$response = $this->receiveSubscribeMessageResponse(['documents'], $headers, [
|
||||
Query::and([
|
||||
Query::equal('status', ['active']),
|
||||
Query::or([
|
||||
Query::greaterThan('score', 50),
|
||||
Query::startsWith('name', 'test'),
|
||||
]),
|
||||
])->toString(),
|
||||
]);
|
||||
$this->assertEquals('error', $response['type']);
|
||||
$this->assertStringContainsString('not supported in Realtime queries', $response['data']['message']);
|
||||
$this->assertStringContainsString('startsWith', $response['data']['message']);
|
||||
|
||||
// Test 5: Multiple invalid 'queries' in nested structure
|
||||
$response = $this->receiveSubscribeMessageResponse(['documents'], $headers, [
|
||||
Query::and([
|
||||
Query::contains('tags', ['important']),
|
||||
Query::or([
|
||||
Query::endsWith('email', '@example.com'),
|
||||
Query::equal('status', ['active']),
|
||||
]),
|
||||
])->toString(),
|
||||
]);
|
||||
$this->assertEquals('error', $response['type']);
|
||||
$this->assertStringContainsString('not supported in Realtime queries', $response['data']['message']);
|
||||
$this->assertTrue(
|
||||
\str_contains($response['data']['message'], 'contains') ||
|
||||
\str_contains($response['data']['message'], 'endsWith')
|
||||
);
|
||||
}
|
||||
|
||||
public function testProjectChannelWithHeaderOnly(): void
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
$client = $this->getWebsocketWithCustomQuery(
|
||||
[
|
||||
'project' => $projectId,
|
||||
],
|
||||
[
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
'x-appwrite-project' => $projectId,
|
||||
]
|
||||
);
|
||||
|
||||
$response = \json_decode($client->receive(), true);
|
||||
$this->assertSame('connected', $response['type']);
|
||||
$subscribeResponse = $this->sendSubscribeMessage($client, [[
|
||||
'channels' => ['project'],
|
||||
'queries' => [Query::select(['*'])->toString()],
|
||||
]]);
|
||||
$this->assertCount(1, $subscribeResponse['data']['subscriptions']);
|
||||
$this->assertSame(['project'], $subscribeResponse['data']['subscriptions'][0]['channels']);
|
||||
|
||||
$client->close();
|
||||
|
||||
$clientWithQuery = $this->getWebsocketWithCustomQuery(
|
||||
[
|
||||
'project' => $projectId,
|
||||
],
|
||||
[
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
'x-appwrite-project' => $projectId,
|
||||
]
|
||||
);
|
||||
|
||||
$response = \json_decode($clientWithQuery->receive(), true);
|
||||
$this->assertSame('connected', $response['type']);
|
||||
$subscribeResponseWithQuery = $this->sendSubscribeMessage($clientWithQuery, [[
|
||||
'channels' => ['project'],
|
||||
'queries' => [Query::select(['*'])->toString()],
|
||||
]]);
|
||||
$this->assertCount(1, $subscribeResponseWithQuery['data']['subscriptions']);
|
||||
$this->assertSame(['project'], $subscribeResponseWithQuery['data']['subscriptions'][0]['channels']);
|
||||
|
||||
$clientWithQuery->close();
|
||||
}
|
||||
|
||||
public function testQueryMessageFiltersEvents(): void
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$userId = $user['$id'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
// Setup database and collection
|
||||
$database = $this->client->call(Client::METHOD_POST, '/databases', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Query Message Test DB',
|
||||
]);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Query Message Test Collection',
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($userId)),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'key' => 'status',
|
||||
'size' => 256,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $collectionId, $projectId) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/status', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
$this->assertEquals('available', $response['body']['status']);
|
||||
}, 30000, 250);
|
||||
|
||||
$targetDocumentId = ID::unique();
|
||||
$otherDocumentId = ID::unique();
|
||||
|
||||
$client = $this->getWebsocket(['documents'], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
], null, [
|
||||
Query::equal('$id', [$targetDocumentId])->toString(),
|
||||
]);
|
||||
|
||||
// Create matching document - should receive event
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $targetDocumentId,
|
||||
'data' => [
|
||||
'status' => 'active',
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$event = \json_decode($client->receive(), true);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertEquals($targetDocumentId, $event['data']['payload']['$id']);
|
||||
|
||||
// Create non-matching document - should NOT receive event
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $otherDocumentId,
|
||||
'data' => [
|
||||
'status' => 'inactive',
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
try {
|
||||
$client->receive();
|
||||
$this->fail('Expected TimeoutException - event should be filtered by updated query');
|
||||
} catch (TimeoutException $e) {
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
$client->close();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 new secret persisted via GET
|
||||
$get = $this->getWebhook($webhookId);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertNotEquals($originalSignatureKey, $get['body']['signatureKey']);
|
||||
$this->assertNotEquals($originalSecret, $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']);
|
||||
|
||||
@@ -934,11 +934,11 @@ 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->assertNotEmpty($get['body']['secret']);
|
||||
$this->assertEquals(128, \strlen($get['body']['secret']));
|
||||
$this->assertEquals(0, $get['body']['attempts']);
|
||||
$this->assertEquals('', $get['body']['logs']);
|
||||
|
||||
@@ -1043,9 +1043,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 +1247,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 +1262,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 +1503,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->assertNotEmpty($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 +1779,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): mixed
|
||||
{
|
||||
$params = [
|
||||
'webhookId' => $webhookId,
|
||||
@@ -1543,14 +1791,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_POST, '/webhooks', array_merge([
|
||||
@@ -1561,7 +1809,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 +1820,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,9 +1838,9 @@ trait WebhooksBase
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
protected function updateWebhookSignature(string $webhookId): mixed
|
||||
protected function updateWebhookSecret(string $webhookId): mixed
|
||||
{
|
||||
$webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
|
||||
$webhook = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\SDK\Specification;
|
||||
|
||||
use Appwrite\SDK\Specification\Format;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\DI\Container;
|
||||
|
||||
class TestFormat extends Format
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'test';
|
||||
}
|
||||
|
||||
public function parse(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function requestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array
|
||||
{
|
||||
return $this->getRequestParameterConfig($service, $method, $param, $optional, $nullable, $default);
|
||||
}
|
||||
}
|
||||
|
||||
class FormatTest extends TestCase
|
||||
{
|
||||
private TestFormat $format;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->format = new TestFormat(new Container(), [], [], [], [], 0, 'console');
|
||||
}
|
||||
|
||||
public function testProjectRequestParameterOverrides(): void
|
||||
{
|
||||
$createWebPlatform = $this->format->requestParameterConfig('project', 'createWebPlatform', 'hostname', true, false, '');
|
||||
$updateWebPlatform = $this->format->requestParameterConfig('project', 'updateWebPlatform', 'hostname', true, false, '');
|
||||
$listPlatforms = $this->format->requestParameterConfig('project', 'listPlatforms', 'queries', true, false, []);
|
||||
|
||||
$this->assertTrue($createWebPlatform['required']);
|
||||
$this->assertFalse($createWebPlatform['emitDefault']);
|
||||
$this->assertTrue($updateWebPlatform['required']);
|
||||
$this->assertFalse($updateWebPlatform['emitDefault']);
|
||||
$this->assertTrue($listPlatforms['emitDefault']);
|
||||
}
|
||||
|
||||
public function testProjectPlatformResponseTypeUsesSharedEnumName(): void
|
||||
{
|
||||
$this->assertSame('PlatformType', $this->format->getResponseEnumName('platformAndroid', 'type'));
|
||||
$this->assertSame('PlatformType', $this->format->getResponseEnumName('platformWeb', 'type'));
|
||||
$this->assertSame('PlatformType', $this->format->getResponseEnumName('platformApple', 'type'));
|
||||
$this->assertSame('PlatformType', $this->format->getResponseEnumName('platformWindows', 'type'));
|
||||
$this->assertSame('PlatformType', $this->format->getResponseEnumName('platformLinux', 'type'));
|
||||
$this->assertNull($this->format->getResponseEnumName('platformList', 'type'));
|
||||
}
|
||||
|
||||
public function testExistingResponseEnumMappingsRemainUnchanged(): void
|
||||
{
|
||||
$this->assertSame('HealthCheckStatus', $this->format->getResponseEnumName('healthStatus', 'status'));
|
||||
$this->assertNull($this->format->getResponseEnumName('key', 'name'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user