diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba7c4cc1be..ac16c80264 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -427,6 +427,7 @@ jobs:
FunctionsSchedule,
GraphQL,
Health,
+ Advisor,
Locale,
Projects,
Realtime,
@@ -449,6 +450,7 @@ jobs:
paratest_processes: 3
timeout_minutes: 30
- service: TablesDB
+ runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
diff --git a/AGENTS.md b/AGENTS.md
index 4c5db871d7..b84bc89c3b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -115,6 +115,14 @@ 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.
+## Tracing with Utopia Span
+
+In handlers, only call `Span::add($key, $value)`. **Never** call `Span::init`, `Span::error`, or `Span::finish` -- lifecycle is owned by the entry-point harness (`app/http.php`, `app/worker.php`, `app/realtime.php`, `Bus::dispatch`). For selective export, filter in the sampler in `app/init/span.php`.
+
+Keys are `snake_case` with dots only for child relationships: `project.id` (id of project), `storage.bucket.id`. No dot otherwise: `inbound_bytes`, not `inbound.bytes`. No camelCase, no bare top-level keys (`function.id`, not `functionId`).
+
+Cross-cutting identifiers (`project.id`, `function.id`, `user.id`) live at the top level, not under a subsystem (no `realtime.project.id`). The trace sampler and downstream filters look them up by the canonical key.
+
## 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.
diff --git a/app/cli.php b/app/cli.php
index ada155c4dc..8dfbaf4e9a 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -4,8 +4,8 @@ require_once __DIR__ . '/init.php';
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -281,9 +281,10 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$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']);
+$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+), ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php
index 748211f222..7496b7a9a7 100644
--- a/app/config/collections/platform.php
+++ b/app/config/collections/platform.php
@@ -1956,6 +1956,440 @@ $platformCollections = [
'attributes' => [],
'indexes' => []
],
+
+ 'reports' => [
+ '$collection' => ID::custom(Database::METADATA),
+ '$id' => ID::custom('reports'),
+ 'name' => 'Reports',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('projectInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('projectId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('appInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('appId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('type'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('title'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('summary'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Resource type the report is about. Plural noun, e.g. databases, sites, urls.
+ '$id' => ID::custom('targetType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Free-form target identifier (URL for lighthouse, resource ID for db).
+ // Indexed by `_key_project_target` with an explicit prefix length.
+ '$id' => ID::custom('target'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Category strings, e.g. 'performance', 'accessibility'. Native array
+ // column — we never query on individual entries (MySQL JSON-array
+ // indexes are weak), this is read+rewrite only.
+ '$id' => ID::custom('categories'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ // Virtual attribute — insights live in the `insights` collection
+ // back-referenced by `reportInternalId`. The subQuery filter joins
+ // them at read time.
+ '$id' => ID::custom('insights'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['subQueryReportInsights'],
+ ],
+ [
+ '$id' => ID::custom('analyzedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_project_app_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'appInternalId', 'type'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_target'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'appInternalId', 'targetType', 'target'],
+ 'lengths' => [null, null, null, 700],
+ 'orders' => [],
+ ],
+ ],
+ ],
+
+ 'insights' => [
+ '$collection' => ID::custom(Database::METADATA),
+ '$id' => ID::custom('insights'),
+ 'name' => 'Insights',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('projectInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('projectId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('reportInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('reportId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('type'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('severity'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('status'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => 'active',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('title'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('summary'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('ctas'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => ID::custom('analyzedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('dismissedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('dismissedBy'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_project_report'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'reportInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_resource'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'resourceType', 'resourceId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_parent_resource'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'parentResourceType', 'parentResourceId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'type'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_severity'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'severity'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_status'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'status'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_dismissedAt'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'dismissedAt'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC, Database::ORDER_DESC],
+ ],
+ ],
+ ],
+
];
// Organization API keys subquery
diff --git a/app/config/errors.php b/app/config/errors.php
index e7b6839a20..42ce9ac91b 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -1453,4 +1453,28 @@ return [
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
+
+ /** Advisor */
+ Exception::INSIGHT_NOT_FOUND => [
+ 'name' => Exception::INSIGHT_NOT_FOUND,
+ 'description' => 'Insight with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::INSIGHT_ALREADY_EXISTS => [
+ 'name' => Exception::INSIGHT_ALREADY_EXISTS,
+ 'description' => 'Insight with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'code' => 409,
+ ],
+
+ /** Reports */
+ Exception::REPORT_NOT_FOUND => [
+ 'name' => Exception::REPORT_NOT_FOUND,
+ 'description' => 'Report with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::REPORT_ALREADY_EXISTS => [
+ 'name' => Exception::REPORT_ALREADY_EXISTS,
+ 'description' => 'Report with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'code' => 409,
+ ],
];
diff --git a/app/config/events.php b/app/config/events.php
index 11dc2e0e4a..2825562ab7 100644
--- a/app/config/events.php
+++ b/app/config/events.php
@@ -426,5 +426,33 @@ return [
'update' => [
'$description' => 'This event triggers when a proxy rule is updated.',
]
- ]
+ ],
+ 'reports' => [
+ '$model' => Response::MODEL_REPORT,
+ '$resource' => true,
+ '$description' => 'This event triggers on any report event.',
+ 'create' => [
+ '$description' => 'This event triggers when a report is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a report is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a report is deleted.',
+ ],
+ 'insights' => [
+ '$model' => Response::MODEL_INSIGHT,
+ '$resource' => true,
+ '$description' => 'This event triggers on any insight event.',
+ 'create' => [
+ '$description' => 'This event triggers when an insight is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when an insight is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when an insight is deleted.',
+ ],
+ ],
+ ],
];
diff --git a/app/config/roles.php b/app/config/roles.php
index 04175ac1d5..cb4b178a29 100644
--- a/app/config/roles.php
+++ b/app/config/roles.php
@@ -103,6 +103,10 @@ $admins = [
'tokens.write',
'schedules.read',
'schedules.write',
+ 'insights.read',
+ 'insights.write',
+ 'reports.read',
+ 'reports.write',
];
return [
diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php
index 6c7281f4b9..3d8998fb2f 100644
--- a/app/config/scopes/project.php
+++ b/app/config/scopes/project.php
@@ -361,4 +361,22 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
+
+ // Advisor
+ 'insights.read' => [
+ 'description' => 'Access to read insights under Advisor service.',
+ 'category' => 'Advisor',
+ ],
+ 'insights.write' => [
+ 'description' => 'Reserved for Advisor insight ingestion outside CE.',
+ 'category' => 'Advisor',
+ ],
+ 'reports.read' => [
+ 'description' => 'Access to read reports under Advisor service.',
+ 'category' => 'Advisor',
+ ],
+ 'reports.write' => [
+ 'description' => 'Access to delete reports under Advisor service.',
+ 'category' => 'Advisor',
+ ],
];
diff --git a/app/config/services.php b/app/config/services.php
index cf2714f8c5..f829937623 100644
--- a/app/config/services.php
+++ b/app/config/services.php
@@ -308,5 +308,19 @@ return [
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
- ]
+ ],
+ 'advisor' => [
+ 'key' => 'advisor',
+ 'name' => 'Advisor',
+ 'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.',
+ 'description' => '/docs/services/advisor.md',
+ 'controller' => '', // Uses modules
+ 'sdk' => true,
+ 'docs' => true,
+ 'docsUrl' => 'https://appwrite.io/docs/server/advisor',
+ 'tests' => true,
+ 'optional' => true,
+ 'icon' => '/images/services/insights.png',
+ 'platforms' => ['server', 'console'],
+ ],
];
diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php
index 494aa11150..8ab30fac99 100644
--- a/app/controllers/api/projects.php
+++ b/app/controllers/api/projects.php
@@ -2,9 +2,6 @@
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Extend\Exception;
-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;
@@ -27,37 +24,6 @@ Http::init()
}
});
-Http::get('/v1/projects/:projectId')
- ->desc('Get project')
- ->groups(['api', 'projects'])
- ->label('scope', 'projects.read')
- ->label('sdk', new Method(
- namespace: 'projects',
- group: 'projects',
- name: 'get',
- description: '/docs/references/projects/get.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'])
- ->inject('response')
- ->inject('dbForPlatform')
- ->action(function (string $projectId, Response $response, Database $dbForPlatform) {
-
- $project = $dbForPlatform->getDocument('projects', $projectId);
-
- if ($project->isEmpty()) {
- throw new Exception(Exception::PROJECT_NOT_FOUND);
- }
-
- $response->dynamic($project, Response::MODEL_PROJECT);
- });
-
// Backwards compatibility
Http::patch('/v1/projects/:projectId/oauth2')
->desc('Update project OAuth2')
diff --git a/app/controllers/general.php b/app/controllers/general.php
index bc63d200d7..dbcfa7f754 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -29,6 +29,7 @@ use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
+use Appwrite\Utopia\Request\Filters\V26 as RequestV26;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -40,6 +41,7 @@ use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
+use Appwrite\Utopia\Response\Filters\V26 as ResponseV26;
use Appwrite\Utopia\View;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
@@ -914,6 +916,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.4', '<')) {
$request->addFilter(new RequestV25());
}
+ if (version_compare($requestFormat, '1.9.5', '<')) {
+ $request->addFilter(new RequestV26());
+ }
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -938,6 +943,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
+ if (version_compare($responseFormat, '1.9.5', '<')) {
+ $response->addFilter(new ResponseV26());
+ }
if (version_compare($responseFormat, '1.9.4', '<')) {
$response->addFilter(new ResponseV25());
}
@@ -1274,7 +1282,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
- $resolvedUser = $utopia->getResource('user');
+ $resolvedUser = $utopia->context()->get('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1293,7 +1301,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
- $user = $utopia->getResource('user');
+ $user = $utopia->context()->get('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1494,7 +1502,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
- $cors = $utopia->getResource('cors');
+ $cors = $utopia->context()->get('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index b43ad517a5..6f808296b0 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -7,10 +7,11 @@ use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
use Appwrite\Event\Message\Audit as AuditMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Audit;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@@ -567,7 +568,7 @@ Http::init()
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('usage')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('dbForProject')
->inject('resourceToken')
->inject('mode')
@@ -577,7 +578,7 @@ Http::init()
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
- ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
+ ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -626,10 +627,6 @@ Http::init()
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
- $queueForFunctions->setProject($project);
-
- /* Auto-set platforms */
- $queueForFunctions->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
@@ -823,7 +820,7 @@ Http::shutdown()
->inject('publisherForUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
@@ -833,7 +830,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
- ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
+ ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -862,9 +859,15 @@ Http::shutdown()
if (! empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
diff --git a/app/http.php b/app/http.php
index b72f3b7f34..6dc415f000 100644
--- a/app/http.php
+++ b/app/http.php
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
-$registerRequestResources = require __DIR__ . '/init/resources/request.php';
+$setRequestContext = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
+use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -57,7 +58,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
-$swooleAdapter = new Server(
+$swoole = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -69,10 +70,10 @@ $swooleAdapter = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
- container: $container,
+ resources: $container,
);
-$http = $swooleAdapter->getServer();
+$http = $swoole->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -190,13 +191,11 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
-$container->set('bus', function ($register) use ($swooleAdapter) {
- return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
-}, ['register']);
+$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
include __DIR__ . '/controllers/general.php';
-function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
+function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -205,7 +204,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
while (true) {
try {
$attempts++;
- $resource = $app->getResource($resourceKey);
+ $resource = $resources->get($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -288,23 +287,21 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
-$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
- $app = new Http($swooleAdapter, 'UTC');
-
+$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
- $pools = $app->getResource('pools');
+ $pools = $container->get('pools');
- go(function () use ($app, $pools) {
+ go(function () use ($container, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
- createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
+ createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
- createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
- $authorization = $app->getResource('authorization');
+ createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
+ $authorization = $container->get('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -416,7 +413,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
- $cache = $app->getResource('cache');
+ $cache = $container->get('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -502,7 +499,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
-$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
+$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -522,21 +519,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
- $requestContainer = $swooleAdapter->getContainer();
- $requestContainer->set('container', fn () => $requestContainer);
- $requestContainer->set('request', fn () => $request);
- $requestContainer->set('response', fn () => $response);
+ $app = new Http($swoole, 'UTC');
+ $app->context()->set('request', fn () => $request);
+ $app->context()->set('response', fn () => $response);
+ $app->context()->set('utopia', fn () => $app);
- $app = new Http($swooleAdapter, 'UTC');
- $requestContainer->set('utopia', fn () => $app);
-
- $registerRequestResources($requestContainer);
+ $setRequestContext($app->context());
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
- $authorization = $app->getResource('authorization');
+ $authorization = $app->context()->get('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -552,18 +546,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
- $logger = $app->getResource("logger");
+ $logger = $app->context()->get("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
- $user = $app->getResource('user');
+ $user = $app->context()->get('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
- $log = $app->getResource("log");
+ $log = $app->context()->get("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -642,18 +636,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
-$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
+$http->on(Constant::EVENT_TASK, function () use ($container) {
$lastSyncUpdate = null;
- $app = new Http($swooleAdapter, 'UTC');
-
/** @var Utopia\Database\Database $dbForPlatform */
- $dbForPlatform = $app->getResource('dbForPlatform');
+ $dbForPlatform = $container->get('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
- $riskyDomains = $app->getResource('riskyDomains');
+ $riskyDomains = $container->get('riskyDomains');
- Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
+ Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -670,7 +662,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
}
$results = [];
try {
- $authorization = $app->getResource('authorization');
+ $authorization = $container->get('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -720,4 +712,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
-$swooleAdapter->start();
+$swoole->start();
diff --git a/app/init/constants.php b/app/init/constants.php
index 8bc9b6a826..966035b956 100644
--- a/app/init/constants.php
+++ b/app/init/constants.php
@@ -1,5 +1,11 @@
value, // legacy databases.createIndex
+ InsightType::TABLES_DB_INDEX->value, // tablesDB.createIndex
+ InsightType::DOCUMENTS_DB_INDEX->value, // documentsDB.createIndex
+ InsightType::VECTORS_DB_INDEX->value, // vectorsDB.createIndex
+ InsightType::DATABASE_PERFORMANCE->value,
+ InsightType::SITE_PERFORMANCE->value,
+ InsightType::SITE_ACCESSIBILITY->value,
+ InsightType::SITE_SEO->value,
+ InsightType::FUNCTION_PERFORMANCE->value,
+];
+
+// Public API services (SDK namespaces) that an insight CTA's `service` can reference.
+// Analyzers must pick the one matching the engine the resource lives in.
+const ADVISOR_CTA_SERVICES = [
+ InsightCTAService::DATABASES->value, // legacy
+ InsightCTAService::TABLES_DB->value,
+ InsightCTAService::DOCUMENTS_DB->value,
+ InsightCTAService::VECTORS_DB->value,
+];
+
+// Public API method names that an insight CTA's `method` can reference for index suggestions.
+const ADVISOR_CTA_METHODS = [
+ InsightCTAMethod::CREATE_INDEX->value,
+];
+
+// Insight severities
+const ADVISOR_SEVERITIES = [
+ InsightSeverity::INFO->value,
+ InsightSeverity::WARNING->value,
+ InsightSeverity::CRITICAL->value,
+];
+
+// Insight statuses
+const ADVISOR_STATUSES = [
+ InsightStatus::ACTIVE->value,
+ InsightStatus::DISMISSED->value,
+];
+
+// Report types
+const ADVISOR_REPORT_TYPES = [
+ ReportType::LIGHTHOUSE->value,
+ ReportType::AUDIT->value,
+ ReportType::DATABASE_ANALYZER->value,
+];
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
diff --git a/app/init/database/filters.php b/app/init/database/filters.php
index 5a65479424..e171805c47 100644
--- a/app/init/database/filters.php
+++ b/app/init/database/filters.php
@@ -475,3 +475,17 @@ Database::addFilter(
]));
}
);
+
+Database::addFilter(
+ 'subQueryReportInsights',
+ function (mixed $value) {
+ return;
+ },
+ function (mixed $value, Document $document, Database $database) {
+ return $database->getAuthorization()->skip(fn () => $database->find('insights', [
+ Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]),
+ Query::equal('reportInternalId', [$document->getSequence()]),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]));
+ }
+);
diff --git a/app/init/models.php b/app/init/models.php
index 8276e2561e..521a3b77cd 100644
--- a/app/init/models.php
+++ b/app/init/models.php
@@ -92,6 +92,8 @@ use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
+use Appwrite\Utopia\Response\Model\Insight;
+use Appwrite\Utopia\Response\Model\InsightCTA;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -173,12 +175,16 @@ use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
+use Appwrite\Utopia\Response\Model\ProjectAuthMethod;
+use Appwrite\Utopia\Response\Model\ProjectProtocol;
+use Appwrite\Utopia\Response\Model\ProjectService;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
+use Appwrite\Utopia\Response\Model\Report;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -288,6 +294,8 @@ Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICA
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
+Response::setModel(new BaseList('Insights List', Response::MODEL_INSIGHT_LIST, 'insights', Response::MODEL_INSIGHT));
+Response::setModel(new BaseList('Reports List', Response::MODEL_REPORT_LIST, 'reports', Response::MODEL_REPORT));
// Entities
Response::setModel(new Database());
@@ -397,6 +405,9 @@ Response::setModel(new FrameworkAdapter());
Response::setModel(new Deployment());
Response::setModel(new Execution());
Response::setModel(new Project());
+Response::setModel(new ProjectAuthMethod());
+Response::setModel(new ProjectService());
+Response::setModel(new ProjectProtocol());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new EphemeralKey());
@@ -509,6 +520,9 @@ Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
+Response::setModel(new Insight());
+Response::setModel(new InsightCTA());
+Response::setModel(new Report());
// Tests (keep last)
Response::setModel(new Mock());
diff --git a/app/init/resources.php b/app/init/resources.php
index a626b612cb..dbaa89b21d 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -5,6 +5,7 @@ use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
@@ -108,6 +109,10 @@ $container->set('publisherForExecutions', fn (Publisher $publisher) => new Execu
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
+$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
diff --git a/app/init/resources/request.php b/app/init/resources/request.php
index 6ed377d9ae..68f5968519 100644
--- a/app/init/resources/request.php
+++ b/app/init/resources/request.php
@@ -8,7 +8,8 @@ use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -48,6 +49,7 @@ use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
+use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -59,26 +61,18 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
-return function (Container $container): void {
- $container->set('utopia:graphql', function ($utopia) {
- return $utopia;
- }, ['utopia']);
+return function (Container $context): void {
+ $context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
- $container->set('log', fn () => new Log(), []);
+ $context->set('log', fn () => new Log(), []);
- $container->set('logger', function ($register) {
- return $register->get('logger');
- }, ['register']);
+ $context->set('logger', fn ($register) => $register->get('logger'), ['register']);
- $container->set('authorization', function () {
- return new Authorization();
- }, []);
+ $context->set('authorization', fn () => new Authorization(), []);
- $container->set('store', function (): Store {
- return new Store();
- }, []);
+ $context->set('store', fn (): Store => new Store(), []);
- $container->set('proofForPassword', function (): Password {
+ $context->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -92,21 +86,21 @@ return function (Container $container): void {
return $password;
});
- $container->set('proofForToken', function (): Token {
+ $context->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
- $container->set('proofForCode', function (): Code {
+ $context->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
- $container->set('locale', function () {
+ $context->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -114,32 +108,19 @@ return function (Container $container): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
- $container->set('queueForDatabase', function (Publisher $publisher) {
- return new EventDatabase($publisher);
- }, ['publisher']);
- $container->set('queueForDeletes', function (Publisher $publisher) {
- return new Delete($publisher);
- }, ['publisher']);
- $container->set('queueForEvents', function (Publisher $publisher) {
- return new Event($publisher);
- }, ['publisher']);
- $container->set('queueForWebhooks', function (Publisher $publisher) {
- return new Webhook($publisher);
- }, ['publisher']);
- $container->set('queueForRealtime', function () {
- return new Realtime();
- }, []);
- $container->set('usage', function () {
- return new UsageContext();
- }, []);
- $container->set('auditContext', fn () => new AuditContext(), []);
- $container->set('queueForFunctions', function (Publisher $publisher) {
- return new Func($publisher);
- }, ['publisher']);
- $container->set('eventProcessor', function () {
- return new EventProcessor();
- }, []);
- $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
+ $context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
+ $context->set('queueForDeletes', fn (Publisher $publisher) => new Delete($publisher), ['publisher']);
+ $context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
+ $context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
+ $context->set('queueForRealtime', fn () => new Realtime(), []);
+ $context->set('usage', fn () => new UsageContext(), []);
+ $context->set('auditContext', fn () => new AuditContext(), []);
+ $context->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+ ), ['publisher']);
+ $context->set('eventProcessor', fn () => new EventProcessor(), []);
+ $context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -157,7 +138,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'cache', 'authorization']);
- $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
+ $context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -214,7 +195,7 @@ return function (Container $container): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
- $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
+ $context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -246,7 +227,7 @@ return function (Container $container): void {
/**
* List of allowed request hostnames for the request.
*/
- $container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
+ $context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -290,7 +271,7 @@ return function (Container $container): void {
/**
* List of allowed request schemes for the request.
*/
- $container->set('allowedSchemes', function (array $platform, Document $project) {
+ $context->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -310,7 +291,7 @@ return function (Container $container): void {
/**
* Whether the request origin is verified against the request hostname.
*/
- $container->set('domainVerification', function (Request $request) {
+ $context->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -322,7 +303,7 @@ return function (Container $container): void {
/**
* Cookie domain for the current request.
*/
- $container->set('cookieDomain', function (Request $request, Document $project) {
+ $context->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -356,7 +337,7 @@ return function (Container $container): void {
/**
* Rule associated with a request origin.
*/
- $container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
+ $context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -406,7 +387,7 @@ return function (Container $container): void {
/**
* CORS service
*/
- $container->set('cors', function (array $allowedHostnames) {
+ $context->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -418,23 +399,23 @@ return function (Container $container): void {
);
}, ['allowedHostnames']);
- $container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
- if (! $devKey->isEmpty()) {
- return new URL();
- }
+ $context->set(
+ 'originValidator',
+ fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
+ ? new Origin($allowedHostnames, $allowedSchemes)
+ : new URL(),
+ ['devKey', 'allowedHostnames', 'allowedSchemes']
+ );
- return new Origin($allowedHostnames, $allowedSchemes);
- }, ['devKey', 'allowedHostnames', 'allowedSchemes']);
+ $context->set(
+ 'redirectValidator',
+ fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
+ ? new Redirect($allowedHostnames, $allowedSchemes)
+ : new URL(),
+ ['devKey', 'allowedHostnames', 'allowedSchemes']
+ );
- $container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
- if (! $devKey->isEmpty()) {
- return new URL();
- }
-
- return new Redirect($allowedHostnames, $allowedSchemes);
- }, ['devKey', 'allowedHostnames', 'allowedSchemes']);
-
- $container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
+ $context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -605,7 +586,7 @@ return function (Container $container): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
- $container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
+ $context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -638,7 +619,7 @@ return function (Container $container): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
- $container->set('session', function (User $user, Store $store, Token $proofForToken) {
+ $context->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -659,7 +640,7 @@ return function (Container $container): void {
return;
}, ['user', 'store', 'proofForToken']);
- $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
+ $context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -715,7 +696,7 @@ return function (Container $container): void {
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
- $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
+ $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -727,9 +708,15 @@ return function (Container $container): void {
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
@@ -909,7 +896,6 @@ return function (Container $container): void {
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
- $queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
@@ -924,7 +910,7 @@ return function (Container $container): void {
$document,
$response,
$queueForEventsClone->from($queueForEvents),
- $queueForFunctions->from($queueForEvents),
+ $publisherForFunctions,
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
@@ -933,9 +919,9 @@ return function (Container $container): void {
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
- }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
+ }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
- $container->set('schema', function ($utopia, $dbForProject, $authorization) {
+ $context->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1022,13 +1008,9 @@ return function (Container $container): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
- $container->set('audit', function ($dbForProject) {
- $adapter = new AdapterDatabase($dbForProject);
+ $context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
- return new Audit($adapter);
- }, ['dbForProject']);
-
- $container->set('mode', function ($request, Document $project) {
+ $context->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1046,7 +1028,7 @@ return function (Container $container): void {
return $mode;
}, ['request', 'project']);
- $container->set('requestTimestamp', function ($request) {
+ $context->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1061,7 +1043,7 @@ return function (Container $container): void {
return $requestTimestamp;
}, ['request']);
- $container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
+ $context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1110,7 +1092,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
- $container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
+ $context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1153,7 +1135,7 @@ return function (Container $container): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
- $container->set('previewHostname', function (Request $request, ?Key $apiKey) {
+ $context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1172,7 +1154,7 @@ return function (Container $container): void {
return '';
}, ['request', 'apiKey']);
- $container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
+ $context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1206,7 +1188,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'team', 'user']);
- $container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
+ $context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1273,10 +1255,10 @@ return function (Container $container): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
- $container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
+ $context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
- $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
+ $databaseDSN = $database->getAttribute('database') ?: $project->getAttribute('database', '');
$databaseType = $database->getAttribute('type', '');
try {
@@ -1435,35 +1417,27 @@ return function (Container $container): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
- $container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
- return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
- }, ['dbForProject', 'authorization', 'getDatabasesDB']);
+ $context->set(
+ 'transactionState',
+ fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
+ ['dbForProject', 'authorization', 'getDatabasesDB']
+ );
- $container->set('executionsRetentionCount', function (Document $project, array $plan) {
- if ($project->getId() === 'console' || empty($plan)) {
- return 0;
- }
+ $context->set(
+ 'executionsRetentionCount',
+ fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
+ ? 0
+ : (int) ($plan['executionsRetentionCount'] ?? 100),
+ ['project', 'plan']
+ );
- return (int) ($plan['executionsRetentionCount'] ?? 100);
- }, ['project', 'plan']);
+ $context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
- $container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForSites', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
-
- $container->set('embeddingAgent', function ($register) {
+ $context->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
diff --git a/app/init/span.php b/app/init/span.php
index 8afa01b2df..f6871badfa 100644
--- a/app/init/span.php
+++ b/app/init/span.php
@@ -3,11 +3,30 @@
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
+use Utopia\System\System;
Span::setStorage(new Storage\Coroutine());
-Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
+
+// Resolve trace filters once at boot to avoid repeated env lookups per span.
+$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
+$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
+$traceEnabled = $traceProjectId !== '' || $traceFunctionId !== '';
+
+Span::addExporter(new Exporter\Pretty(), function (Span $span) use ($traceEnabled, $traceProjectId, $traceFunctionId): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
+
+ // Selective tracing: when _APP_TRACE_PROJECT_ID / _APP_TRACE_FUNCTION_ID are set,
+ // only export spans tagged with matching project.id / function.id.
+ if ($traceEnabled) {
+ if ($traceProjectId !== '' && $span->get('project.id') !== $traceProjectId) {
+ return false;
+ }
+ if ($traceFunctionId !== '' && $span->get('function.id') !== $traceFunctionId) {
+ return false;
+ }
+ }
+
return true;
});
diff --git a/app/init/worker/message.php b/app/init/worker/message.php
index 791bf5edf0..d4aea0c51e 100644
--- a/app/init/worker/message.php
+++ b/app/init/worker/message.php
@@ -3,7 +3,7 @@
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -23,6 +23,7 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
+use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
@@ -343,9 +344,10 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
- $container->set('queueForFunctions', function (Publisher $publisher) {
- return new Func($publisher);
- }, ['publisher']);
+ $container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+ ), ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
diff --git a/app/realtime.php b/app/realtime.php
index 826d751b14..9f42d77461 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -728,8 +728,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = false;
Span::init('realtime.open');
- Span::add('realtime.connectionId', $connection);
- Span::add('realtime.inboundBytes', $rawSize);
+ Span::add('realtime.connection.id', $connection);
+ Span::add('realtime.inbound_bytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
@@ -936,16 +936,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Span::error($th);
} finally {
Span::add('realtime.success', $success);
- Span::add('realtime.responseCode', $responseCode);
- Span::add('realtime.subscriptionMode', $subscriptionMode);
- Span::add('realtime.channelCount', $channelCount);
- Span::add('realtime.subscriptionCount', $subscriptionCount);
- Span::add('realtime.outboundBytes', $outboundBytes);
+ Span::add('realtime.response_code', $responseCode);
+ Span::add('realtime.subscription_mode', $subscriptionMode);
+ Span::add('realtime.channel_count', $channelCount);
+ Span::add('realtime.subscription_count', $subscriptionCount);
+ Span::add('realtime.outbound_bytes', $outboundBytes);
if (!empty($project?->getId())) {
- Span::add('realtime.projectId', $project->getId());
+ Span::add('project.id', $project->getId());
}
if (!empty($logUser?->getId())) {
- Span::add('realtime.userId', $logUser->getId());
+ Span::add('user.id', $logUser->getId());
}
Span::current()?->finish();
}
@@ -965,9 +965,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$success = false;
Span::init('realtime.message');
- Span::add('realtime.connectionId', $connection);
- Span::add('realtime.inboundBytes', $rawSize);
- Span::add('realtime.containerId', $containerId);
+ Span::add('realtime.connection.id', $connection);
+ Span::add('realtime.inbound_bytes', $rawSize);
+ Span::add('realtime.container.id', $containerId);
try {
$response = new Response(new SwooleResponse());
@@ -1352,15 +1352,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
Span::error($th);
} finally {
Span::add('realtime.success', $success);
- Span::add('realtime.responseCode', $responseCode);
- Span::add('realtime.subscriptionDelta', $subscriptionDelta);
- Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
- Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
- Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
- Span::add('realtime.outboundBytes', $outboundBytes);
- Span::add('realtime.projectId', $project?->getId() ?? $projectId);
- Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
- Span::add('realtime.messageType', $messageType);
+ Span::add('realtime.response_code', $responseCode);
+ Span::add('realtime.subscription_delta', $subscriptionDelta);
+ Span::add('realtime.subscriptions_requested', $subscriptionsRequested);
+ Span::add('realtime.subscriptions_removed', $subscriptionsRemoved);
+ Span::add('realtime.subscribe.subscriptions_count', $subscriptionsRequested);
+ Span::add('realtime.outbound_bytes', $outboundBytes);
+ Span::add('project.id', $project?->getId() ?? $projectId);
+ Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
+ Span::add('realtime.message_type', $messageType);
Span::current()?->finish();
}
});
@@ -1372,7 +1372,7 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$success = false;
Span::init('realtime.close');
- Span::add('realtime.connectionId', $connection);
+ Span::add('realtime.connection.id', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -1411,12 +1411,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
Span::add('realtime.success', $success);
if (!empty($projectId)) {
- Span::add('realtime.projectId', $projectId);
+ Span::add('project.id', $projectId);
}
if (!empty($userId)) {
- Span::add('realtime.userId', $userId);
+ Span::add('user.id', $userId);
}
- Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
+ Span::add('realtime.subscriptions_before_close', $subscriptionsBeforeClose);
Span::current()?->finish();
}
diff --git a/app/worker.php b/app/worker.php
index 12b822c4eb..169b8e9770 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -16,6 +16,7 @@ use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Server;
+use Utopia\Span\Span;
use Utopia\System\System;
Runtime::enableCoroutine();
@@ -91,8 +92,13 @@ $adapter = new Swoole(
$worker = new Server($adapter, $container);
try {
- $worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
+ $worker->init()->action(function () use ($worker, $registerWorkerMessageResources, $queueName) {
$registerWorkerMessageResources($worker->getContainer());
+ Span::init("worker.{$queueName}");
+ });
+
+ $worker->shutdown()->action(function () {
+ Span::current()?->finish();
});
$container->set('bus', function ($register) use ($worker) {
@@ -120,6 +126,8 @@ $worker
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
+ Span::error($error);
+
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
diff --git a/composer.json b/composer.json
index 7d68a838f8..7cd048726c 100644
--- a/composer.json
+++ b/composer.json
@@ -54,20 +54,20 @@
"utopia-php/abuse": "1.3.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
- "utopia-php/audit": "2.2.*",
+ "utopia-php/audit": "2.3.*",
"utopia-php/auth": "0.5.*",
- "utopia-php/cache": "1.0.*",
+ "utopia-php/cache": "^2.1",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
- "utopia-php/domains": "1.*",
- "utopia-php/emails": "0.6.*",
- "utopia-php/dns": "1.6.*",
+ "utopia-php/domains": "2.*",
+ "utopia-php/emails": "0.7.*",
+ "utopia-php/dns": "1.7.*",
"utopia-php/dsn": "0.2.1",
- "utopia-php/http": "0.34.*",
+ "utopia-php/http": "^2.0@RC",
"utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
@@ -75,7 +75,7 @@
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
- "utopia-php/platform": "0.13.*",
+ "utopia-php/platform": "^1.0@RC",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
@@ -85,7 +85,7 @@
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
- "utopia-php/vcs": "3.*",
+ "utopia-php/vcs": "4.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
diff --git a/composer.lock b/composer.lock
index c217939534..db2c7cb567 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d58736fec3028d1f9aedd055e6d82684",
+ "content-hash": "9377e1b56bca8dbaf213ee3572ca15c0",
"packages": [
{
"name": "adhocore/jwt",
@@ -3510,22 +3510,23 @@
},
{
"name": "utopia-php/audit",
- "version": "2.2.3",
+ "version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
- "reference": "95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c"
+ "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/audit/zipball/95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c",
- "reference": "95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c",
+ "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3",
+ "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3",
"shasum": ""
},
"require": {
- "php": ">=8.0",
+ "php": ">=8.4",
"utopia-php/database": "5.*",
"utopia-php/fetch": "^1.1",
+ "utopia-php/query": "0.1.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
@@ -3553,9 +3554,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
- "source": "https://github.com/utopia-php/audit/tree/2.2.3"
+ "source": "https://github.com/utopia-php/audit/tree/2.3.2"
},
- "time": "2026-05-08T10:38:23+00:00"
+ "time": "2026-05-14T04:00:37+00:00"
},
{
"name": "utopia-php/auth",
@@ -3614,23 +3615,24 @@
},
{
"name": "utopia-php/cache",
- "version": "1.0.3",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
- "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa"
+ "reference": "fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/cache/zipball/ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
- "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
+ "url": "https://api.github.com/repos/utopia-php/cache/zipball/fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e",
+ "reference": "fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-memcached": "*",
"ext-redis": "*",
- "php": ">=8.0",
+ "php": ">=8.3",
+ "utopia-php/circuit-breaker": "0.3.*",
"utopia-php/pools": "1.*",
"utopia-php/telemetry": "*"
},
@@ -3638,6 +3640,7 @@
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.3",
+ "swoole/ide-helper": "^6.0",
"vimeo/psalm": "4.13.1"
},
"type": "library",
@@ -3660,9 +3663,71 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
- "source": "https://github.com/utopia-php/cache/tree/1.0.3"
+ "source": "https://github.com/utopia-php/cache/tree/2.1.0"
},
- "time": "2026-05-11T11:02:13+00:00"
+ "time": "2026-05-12T15:03:23+00:00"
+ },
+ {
+ "name": "utopia-php/circuit-breaker",
+ "version": "0.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/circuit-breaker.git",
+ "reference": "064243c1667778c00abf027ff53a735a228776de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/circuit-breaker/zipball/064243c1667778c00abf027ff53a735a228776de",
+ "reference": "064243c1667778c00abf027ff53a735a228776de",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.29",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^10.0",
+ "utopia-php/telemetry": "0.2.*"
+ },
+ "suggest": {
+ "ext-opentelemetry": "Required by utopia-php/telemetry when using OpenTelemetry metrics.",
+ "ext-protobuf": "Required by utopia-php/telemetry when using OpenTelemetry metrics.",
+ "ext-redis": "Required when using Utopia\\CircuitBreaker\\Adapter\\Redis with the phpredis extension.",
+ "ext-swoole": "Required when using Utopia\\CircuitBreaker\\Adapter\\SwooleTable.",
+ "utopia-php/telemetry": "Required when passing telemetry adapters or running the local telemetry demo."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\CircuitBreaker\\": "src/CircuitBreaker"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Team Appwrite",
+ "email": "team@appwrite.io"
+ }
+ ],
+ "description": "Light & simple Circuit Breaker for PHP to prevent cascading failures in distributed systems.",
+ "keywords": [
+ "circuit-breaker",
+ "fault-tolerance",
+ "framework",
+ "php",
+ "resilience",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/circuit-breaker/issues",
+ "source": "https://github.com/utopia-php/circuit-breaker/tree/0.3.0"
+ },
+ "time": "2026-05-12T04:27:08+00:00"
},
{
"name": "utopia-php/cli",
@@ -3858,16 +3923,16 @@
},
{
"name": "utopia-php/database",
- "version": "5.7.0",
+ "version": "5.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511"
+ "reference": "3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
- "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696",
+ "reference": "3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696",
"shasum": ""
},
"require": {
@@ -3876,7 +3941,7 @@
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.4",
- "utopia-php/cache": "1.*",
+ "utopia-php/cache": "^2.0",
"utopia-php/console": "0.1.*",
"utopia-php/mongo": "1.*",
"utopia-php/pools": "1.*",
@@ -3912,9 +3977,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/5.7.0"
+ "source": "https://github.com/utopia-php/database/tree/5.8.0"
},
- "time": "2026-05-06T01:04:08+00:00"
+ "time": "2026-05-12T12:52:44+00:00"
},
{
"name": "utopia-php/detector",
@@ -4014,21 +4079,21 @@
},
{
"name": "utopia-php/dns",
- "version": "1.6.6",
+ "version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/dns.git",
- "reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
+ "reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
- "reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
+ "url": "https://api.github.com/repos/utopia-php/dns/zipball/90bf1bc4a51ceca93590d09e7365317b28d1eb89",
+ "reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89",
"shasum": ""
},
"require": {
"php": ">=8.3",
- "utopia-php/domains": "1.0.*",
+ "utopia-php/domains": "^2.0",
"utopia-php/span": "1.1.*",
"utopia-php/telemetry": "*",
"utopia-php/validators": "0.*"
@@ -4065,27 +4130,27 @@
],
"support": {
"issues": "https://github.com/utopia-php/dns/issues",
- "source": "https://github.com/utopia-php/dns/tree/1.6.6"
+ "source": "https://github.com/utopia-php/dns/tree/1.7.0"
},
- "time": "2026-03-27T11:13:50+00:00"
+ "time": "2026-05-13T07:11:31+00:00"
},
{
"name": "utopia-php/domains",
- "version": "1.0.6",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
- "reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6"
+ "reference": "7f76390998359ef67fcea168f614cbd63a4001e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/domains/zipball/c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
- "reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
+ "url": "https://api.github.com/repos/utopia-php/domains/zipball/7f76390998359ef67fcea168f614cbd63a4001e8",
+ "reference": "7f76390998359ef67fcea168f614cbd63a4001e8",
"shasum": ""
},
"require": {
"php": ">=8.2",
- "utopia-php/cache": "1.0.*",
+ "utopia-php/cache": "^2.0",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4127,9 +4192,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
- "source": "https://github.com/utopia-php/domains/tree/1.0.6"
+ "source": "https://github.com/utopia-php/domains/tree/2.0.0"
},
- "time": "2026-04-29T11:08:10+00:00"
+ "time": "2026-05-12T12:52:53+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4180,21 +4245,21 @@
},
{
"name": "utopia-php/emails",
- "version": "0.6.10",
+ "version": "0.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
- "reference": "2e397754ce68c2ba918564b9f31d9923c0a90429"
+ "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/emails/zipball/2e397754ce68c2ba918564b9f31d9923c0a90429",
- "reference": "2e397754ce68c2ba918564b9f31d9923c0a90429",
+ "url": "https://api.github.com/repos/utopia-php/emails/zipball/115e24aa908e2b1f06c7ff3b94434a0bdbed9107",
+ "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107",
"shasum": ""
},
"require": {
"php": ">=8.0",
- "utopia-php/domains": "^1.0",
+ "utopia-php/domains": "^2.0",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4235,9 +4300,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
- "source": "https://github.com/utopia-php/emails/tree/0.6.10"
+ "source": "https://github.com/utopia-php/emails/tree/0.7.0"
},
- "time": "2026-05-08T10:16:22+00:00"
+ "time": "2026-05-13T05:01:26+00:00"
},
{
"name": "utopia-php/fetch",
@@ -4281,16 +4346,16 @@
},
{
"name": "utopia-php/http",
- "version": "0.34.25",
+ "version": "2.0.0-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
- "reference": "76be330d4197bae680eb4ccc29c573456fe91904"
+ "reference": "3e3b431d443844c6bf810120dee735f45880856f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/http/zipball/76be330d4197bae680eb4ccc29c573456fe91904",
- "reference": "76be330d4197bae680eb4ccc29c573456fe91904",
+ "url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f",
+ "reference": "3e3b431d443844c6bf810120dee735f45880856f",
"shasum": ""
},
"require": {
@@ -4331,9 +4396,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
- "source": "https://github.com/utopia-php/http/tree/0.34.25"
+ "source": "https://github.com/utopia-php/http/tree/2.0.0-rc1"
},
- "time": "2026-05-05T04:39:15+00:00"
+ "time": "2026-05-05T15:00:03+00:00"
},
{
"name": "utopia-php/image",
@@ -4657,16 +4722,16 @@
},
{
"name": "utopia-php/platform",
- "version": "0.13.2",
+ "version": "1.0.0-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
- "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9"
+ "reference": "36c0a8b2f3d96ca056d724701a302a127111e933"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
- "reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
+ "url": "https://api.github.com/repos/utopia-php/platform/zipball/36c0a8b2f3d96ca056d724701a302a127111e933",
+ "reference": "36c0a8b2f3d96ca056d724701a302a127111e933",
"shasum": ""
},
"require": {
@@ -4674,7 +4739,7 @@
"ext-redis": "*",
"php": ">=8.3",
"utopia-php/cli": "0.23.3",
- "utopia-php/http": "0.34.25",
+ "utopia-php/http": "^2.0@RC",
"utopia-php/queue": "0.18.2",
"utopia-php/servers": "0.4.0"
},
@@ -4702,9 +4767,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
- "source": "https://github.com/utopia-php/platform/tree/0.13.2"
+ "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc1"
},
- "time": "2026-05-05T06:00:26+00:00"
+ "time": "2026-05-05T15:09:27+00:00"
},
{
"name": "utopia-php/pools",
@@ -4812,6 +4877,52 @@
},
"time": "2020-10-24T07:04:59+00:00"
},
+ {
+ "name": "utopia-php/query",
+ "version": "0.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/query.git",
+ "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/query/zipball/964a10ed3185490505f4c0062f2eb7b89287fb27",
+ "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "laravel/pint": "*",
+ "phpstan/phpstan": "*",
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\Query\\": "src/Query"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A simple library providing a query abstraction for filtering, ordering, and pagination",
+ "keywords": [
+ "framework",
+ "php",
+ "query",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/query/issues",
+ "source": "https://github.com/utopia-php/query/tree/0.1.1"
+ },
+ "time": "2026-03-03T09:05:14+00:00"
+ },
{
"name": "utopia-php/queue",
"version": "0.18.2",
@@ -5238,22 +5349,22 @@
},
{
"name": "utopia-php/vcs",
- "version": "3.2.1",
+ "version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
- "reference": "03ccd12b75d67d29094eb760b468fddde4b6b5e5"
+ "reference": "c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/vcs/zipball/03ccd12b75d67d29094eb760b468fddde4b6b5e5",
- "reference": "03ccd12b75d67d29094eb760b468fddde4b6b5e5",
+ "url": "https://api.github.com/repos/utopia-php/vcs/zipball/c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e",
+ "reference": "c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
- "php": ">=8.0",
- "utopia-php/cache": "1.0.*",
+ "php": ">=8.2",
+ "utopia-php/cache": "^2.0",
"utopia-php/fetch": "^1.1"
},
"require-dev": {
@@ -5281,9 +5392,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
- "source": "https://github.com/utopia-php/vcs/tree/3.2.1"
+ "source": "https://github.com/utopia-php/vcs/tree/4.0.0"
},
- "time": "2026-05-08T10:13:53+00:00"
+ "time": "2026-05-13T04:20:45+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5476,16 +5587,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "1.28.4",
+ "version": "1.29.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "38de925e8c9e7f0f720d45187be54a291aaf696b"
+ "reference": "31248a984a4d478d20a780dda8f5897984ee4e8f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/38de925e8c9e7f0f720d45187be54a291aaf696b",
- "reference": "38de925e8c9e7f0f720d45187be54a291aaf696b",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/31248a984a4d478d20a780dda8f5897984ee4e8f",
+ "reference": "31248a984a4d478d20a780dda8f5897984ee4e8f",
"shasum": ""
},
"require": {
@@ -5521,9 +5632,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.28.4"
+ "source": "https://github.com/appwrite/sdk-generator/tree/1.29.2"
},
- "time": "2026-05-11T13:55:49+00:00"
+ "time": "2026-05-13T04:47:38+00:00"
},
{
"name": "brianium/paratest",
@@ -8455,7 +8566,10 @@
],
"aliases": [],
"minimum-stability": "dev",
- "stability-flags": {},
+ "stability-flags": {
+ "utopia-php/http": 5,
+ "utopia-php/platform": 5
+ },
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
diff --git a/docker-compose.yml b/docker-compose.yml
index 2ae1bf486a..76f06c672a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -255,7 +255,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
- image: appwrite/console:7.8.45
+ image: appwrite/console:8
restart: unless-stopped
networks:
- appwrite
diff --git a/docs/references/advisor/delete-report.md b/docs/references/advisor/delete-report.md
new file mode 100644
index 0000000000..b32ba845e2
--- /dev/null
+++ b/docs/references/advisor/delete-report.md
@@ -0,0 +1 @@
+Delete an analyzer report by its unique ID. Nested insights and CTA metadata are removed asynchronously by the deletes worker.
diff --git a/docs/references/advisor/get-insight.md b/docs/references/advisor/get-insight.md
new file mode 100644
index 0000000000..7e1e795c22
--- /dev/null
+++ b/docs/references/advisor/get-insight.md
@@ -0,0 +1 @@
+Get an insight by its unique ID, scoped to its parent report.
diff --git a/docs/references/advisor/get-report.md b/docs/references/advisor/get-report.md
new file mode 100644
index 0000000000..731c10dc8a
--- /dev/null
+++ b/docs/references/advisor/get-report.md
@@ -0,0 +1 @@
+Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced.
diff --git a/docs/references/advisor/list-insights.md b/docs/references/advisor/list-insights.md
new file mode 100644
index 0000000000..56d6a2fca0
--- /dev/null
+++ b/docs/references/advisor/list-insights.md
@@ -0,0 +1 @@
+List the insights produced under a single analyzer report. You can use the query params to filter your results further.
diff --git a/docs/references/advisor/list-reports.md b/docs/references/advisor/list-reports.md
new file mode 100644
index 0000000000..04b91c541a
--- /dev/null
+++ b/docs/references/advisor/list-reports.md
@@ -0,0 +1 @@
+Get a list of all the project's analyzer reports. You can use the query params to filter your results.
diff --git a/docs/services/advisor.md b/docs/services/advisor.md
new file mode 100644
index 0000000000..2fa3943829
--- /dev/null
+++ b/docs/services/advisor.md
@@ -0,0 +1,3 @@
+The Advisor service provides read access to analyzer reports and their nested insights for a project.
+
+Use the reports endpoints to list and fetch analyzer runs, then use the insights endpoints to inspect individual findings attached to a report.
diff --git a/phpunit.xml b/phpunit.xml
index 9748c5a5c8..32e865fe35 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -38,6 +38,7 @@
./tests/e2e/Services/Messaging
./tests/e2e/Services/Migrations
./tests/e2e/Services/Project
+ ./tests/e2e/Services/Advisor
./tests/e2e/Services/Functions/FunctionsBase.php
./tests/e2e/Services/Functions/FunctionsCustomServerTest.php
./tests/e2e/Services/Functions/FunctionsCustomClientTest.php
diff --git a/src/Appwrite/Advisor/Validator/CTAs.php b/src/Appwrite/Advisor/Validator/CTAs.php
new file mode 100644
index 0000000000..14f7d788e7
--- /dev/null
+++ b/src/Appwrite/Advisor/Validator/CTAs.php
@@ -0,0 +1,83 @@
+allowedServices = $allowedServices ?? ADVISOR_CTA_SERVICES;
+ $this->allowedMethods = $allowedMethods ?? ADVISOR_CTA_METHODS;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->message;
+ }
+
+ public function isArray(): bool
+ {
+ return true;
+ }
+
+ public function getType(): string
+ {
+ return self::TYPE_ARRAY;
+ }
+
+ public function isValid($value): bool
+ {
+ if (!\is_array($value)) {
+ return false;
+ }
+
+ if (\count($value) > $this->maxCount) {
+ $this->message = "A maximum of {$this->maxCount} CTAs are allowed per insight.";
+ return false;
+ }
+
+ foreach ($value as $entry) {
+ if (!\is_array($entry)) {
+ return false;
+ }
+
+ $maxLengths = ['label' => 256, 'service' => 64, 'method' => 64];
+ foreach ($maxLengths as $required => $maxLength) {
+ if (!isset($entry[$required]) || !\is_string($entry[$required]) || $entry[$required] === '') {
+ return false;
+ }
+ if (\strlen($entry[$required]) > $maxLength) {
+ $this->message = "CTA `{$required}` must not exceed {$maxLength} characters.";
+ return false;
+ }
+ }
+
+ if (!empty($this->allowedServices) && !\in_array($entry['service'], $this->allowedServices, true)) {
+ $this->message = "CTA `service` must be one of: " . \implode(', ', $this->allowedServices) . '.';
+ return false;
+ }
+
+ if (!empty($this->allowedMethods) && !\in_array($entry['method'], $this->allowedMethods, true)) {
+ $this->message = "CTA `method` must be one of: " . \implode(', ', $this->allowedMethods) . '.';
+ return false;
+ }
+
+ if (isset($entry['params']) && !\is_array($entry['params']) && !\is_object($entry['params'])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/Appwrite/Bus/Listeners/Log.php b/src/Appwrite/Bus/Listeners/Log.php
index 585d4b09a7..e0376a4f81 100644
--- a/src/Appwrite/Bus/Listeners/Log.php
+++ b/src/Appwrite/Bus/Listeners/Log.php
@@ -8,7 +8,6 @@ use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
-use Utopia\System\System;
class Log extends Listener
{
@@ -34,20 +33,13 @@ class Log extends Listener
{
$project = new Document($event->project);
$execution = new Document($event->execution);
+
if ($execution->getAttribute('resourceType', '') === 'functions') {
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- $resourceId = $execution->getAttribute('resourceId', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
- Span::init('execution.trace.v1_executions_enqueue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $resourceId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('status', $execution->getAttribute('status', ''));
- Span::current()?->finish();
- }
+ Span::add('project.id', $project->getId());
+ Span::add('function.id', $execution->getAttribute('resourceId', ''));
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('execution.status', $execution->getAttribute('status', ''));
}
$publisherForExecutions->enqueue(new ExecutionMessage(
diff --git a/src/Appwrite/Event/Message/Func.php b/src/Appwrite/Event/Message/Func.php
new file mode 100644
index 0000000000..2a2ae9d90f
--- /dev/null
+++ b/src/Appwrite/Event/Message/Func.php
@@ -0,0 +1,92 @@
+platform) ? $this->platform : Config::getParam('platform', []);
+
+ return [
+ 'project' => $this->project?->getArrayCopy(),
+ 'user' => $this->user?->getArrayCopy(),
+ 'userId' => $this->userId,
+ 'function' => $this->function?->getArrayCopy(),
+ 'functionId' => $this->functionId,
+ 'execution' => $this->execution?->getArrayCopy(),
+ 'type' => $this->type,
+ 'jwt' => $this->jwt,
+ 'payload' => $this->payload,
+ 'events' => $this->events,
+ 'body' => $this->body,
+ 'path' => $this->path,
+ 'headers' => $this->headers,
+ 'method' => $this->method,
+ 'platform' => $platform,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: !empty($data['project']) ? new Document($data['project']) : null,
+ user: !empty($data['user']) ? new Document($data['user']) : null,
+ userId: $data['userId'] ?? null,
+ function: !empty($data['function']) ? new Document($data['function']) : null,
+ functionId: $data['functionId'] ?? null,
+ execution: !empty($data['execution']) ? new Document($data['execution']) : null,
+ type: $data['type'] ?? '',
+ jwt: $data['jwt'] ?? '',
+ payload: $data['payload'] ?? [],
+ events: $data['events'] ?? [],
+ body: $data['body'] ?? '',
+ path: $data['path'] ?? '',
+ headers: $data['headers'] ?? [],
+ method: $data['method'] ?? '',
+ platform: $data['platform'] ?? [],
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Func.php b/src/Appwrite/Event/Publisher/Func.php
new file mode 100644
index 0000000000..46f748a59f
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Func.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index 42dcacaf34..a0553d00b8 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -406,6 +406,14 @@ class Exception extends \Exception
public const string TOKEN_EXPIRED = 'token_expired';
public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
+ /** Advisor */
+ public const string INSIGHT_NOT_FOUND = 'insight_not_found';
+ public const string INSIGHT_ALREADY_EXISTS = 'insight_already_exists';
+
+ /** Reports */
+ public const string REPORT_NOT_FOUND = 'report_not_found';
+ public const string REPORT_ALREADY_EXISTS = 'report_already_exists';
+
protected string $type = '';
protected array $errors = [];
protected bool $publish;
diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php
index cabb357607..6e8c30b6ec 100644
--- a/src/Appwrite/GraphQL/Resolvers.php
+++ b/src/Appwrite/GraphQL/Resolvers.php
@@ -6,7 +6,6 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
-use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@@ -53,22 +52,6 @@ class Resolvers
}
}
- /**
- * Get the current request container.
- */
- private static function getResolverContainer(Http $utopia): Container
- {
- $container = $utopia->getResource('container');
-
- if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
- /** @var Container $container */
- return $container;
- }
-
- /** @var callable(): Container $container */
- return $container();
- }
-
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
@@ -95,9 +78,9 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -167,9 +150,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -203,9 +186,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
$beforeResolve = function ($payload) {
return $payload['documents'];
@@ -245,9 +228,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -282,9 +265,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -317,9 +300,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -373,10 +356,9 @@ class Resolvers
}
/** @var Response $resolverResponse */
- $resolverResponse = clone $utopia->getResource('response');
- $container = self::getResolverContainer($utopia);
- $container->set('request', static fn () => $request);
- $container->set('response', static fn () => $resolverResponse);
+ $resolverResponse = clone $utopia->context()->get('response');
+ $utopia->context()->set('request', static fn () => $request);
+ $utopia->context()->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php
index 4ff96fb635..a689655e31 100644
--- a/src/Appwrite/GraphQL/Schema.php
+++ b/src/Appwrite/GraphQL/Schema.php
@@ -84,7 +84,7 @@ class Schema
protected static function api(Http $utopia, callable $complexity): array
{
Mapper::init($utopia
- ->getResource('response')
+ ->context()->get('response')
->getModels());
$queries = [];
diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php
index 55810fd74e..92c7753ad2 100644
--- a/src/Appwrite/GraphQL/Types/Mapper.php
+++ b/src/Appwrite/GraphQL/Types/Mapper.php
@@ -254,7 +254,7 @@ class Mapper
array $injections
): Type {
$validator = \is_callable($validator)
- ? \call_user_func_array($validator, $utopia->getResources($injections))
+ ? \call_user_func_array($validator, \array_map($utopia->context()->get(...), $injections))
: $validator;
$isNullable = $validator instanceof Nullable;
diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php
index 5a9c02a2bd..c4cd2c08d5 100644
--- a/src/Appwrite/Messaging/Adapter/Realtime.php
+++ b/src/Appwrite/Messaging/Adapter/Realtime.php
@@ -774,6 +774,21 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
+ case 'reports':
+ // Plain report event: `reports.{reportId}.{action}`
+ $channels[] = 'reports';
+ if (isset($parts[1])) {
+ $channels[] = 'reports.' . $parts[1];
+ }
+ // Nested insight event: `reports.{reportId}.insights.{insightId}.{action}`
+ if (isset($parts[2]) && $parts[2] === 'insights') {
+ $channels[] = 'reports.' . $parts[1] . '.insights';
+ if (isset($parts[3])) {
+ $channels[] = 'reports.' . $parts[1] . '.insights.' . $parts[3];
+ }
+ }
+ $roles = [Role::team($project->getAttribute('teamId'))->toString()];
+ break;
}
// Action is the last segment for plain CRUD events (e.g. `documents.X.create`),
diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php
index 08e32a9c74..77c62bce96 100644
--- a/src/Appwrite/Migration/Migration.php
+++ b/src/Appwrite/Migration/Migration.php
@@ -97,6 +97,7 @@ abstract class Migration
'1.9.2' => 'V24',
'1.9.3' => 'V24',
'1.9.4' => 'V24',
+ '1.9.5' => 'V24',
];
/**
diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php
index 88788b73fc..a9cd1a8e2f 100644
--- a/src/Appwrite/Platform/Appwrite.php
+++ b/src/Appwrite/Platform/Appwrite.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Account;
+use Appwrite\Platform\Modules\Advisor;
use Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
@@ -42,5 +43,6 @@ class Appwrite extends Platform
$this->addModule(new Webhooks\Module());
$this->addModule(new Migrations\Module());
$this->addModule(new Project\Module());
+ $this->addModule(new Advisor\Module());
}
}
diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php
index 38d61b7d24..26d82adf24 100644
--- a/src/Appwrite/Platform/Installer/Server.php
+++ b/src/Appwrite/Platform/Installer/Server.php
@@ -154,7 +154,7 @@ class Server
$nativeServer = $adapter->getNativeServer();
- $container = $adapter->getContainer();
+ $container = $adapter->resources();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
diff --git a/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php b/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php
new file mode 100644
index 0000000000..31d578a991
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php
@@ -0,0 +1,8 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId/insights/:insightId')
+ ->desc('Get insight')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'insights.read')
+ ->label('resourceType', RESOURCE_TYPE_INSIGHTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'insights',
+ name: 'getInsight',
+ description: '/docs/references/advisor/get-insight.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_INSIGHT,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
+ ->param('insightId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ string $insightId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ // Skip the insights subquery — we only need ownership metadata.
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ $insight = $dbForPlatform->getDocument('insights', $insightId);
+
+ if (
+ $insight->isEmpty()
+ || $insight->getAttribute('projectInternalId') !== $project->getSequence()
+ || $insight->getAttribute('reportInternalId') !== $report->getSequence()
+ ) {
+ throw new Exception(Exception::INSIGHT_NOT_FOUND);
+ }
+
+ $response->dynamic($insight, Response::MODEL_INSIGHT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php b/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php
new file mode 100644
index 0000000000..64d3676c08
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php
@@ -0,0 +1,126 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId/insights')
+ ->desc('List insights')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'insights.read')
+ ->label('resourceType', RESOURCE_TYPE_INSIGHTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'insights',
+ name: 'listInsights',
+ description: '/docs/references/advisor/list-insights.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_INSIGHT_LIST,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
+ ->param('queries', [], new Insights(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Insights::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ array $queries,
+ bool $includeTotal,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ // Skip the insights subquery — we're about to fetch a filtered, paginated slice ourselves.
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
+ $queries[] = Query::equal('reportInternalId', [$report->getSequence()]);
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $insightId = $cursor->getValue();
+ $cursorDocument = $dbForPlatform->getDocument('insights', $insightId);
+
+ if (
+ $cursorDocument->isEmpty()
+ || $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()
+ || $cursorDocument->getAttribute('reportInternalId') !== $report->getSequence()
+ ) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Insight '{$insightId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $insights = $dbForPlatform->find('insights', $queries);
+ $total = $includeTotal ? $dbForPlatform->count('insights', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
+ $response->dynamic(new Document([
+ 'insights' => $insights,
+ 'total' => $total,
+ ]), Response::MODEL_INSIGHT_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php
new file mode 100644
index 0000000000..6b1dfba31b
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php
@@ -0,0 +1,97 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/reports/:reportId')
+ ->desc('Delete report')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.write')
+ ->label('event', 'reports.[reportId].delete')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('audits.event', 'report.delete')
+ ->label('audits.resource', 'report/{request.reportId}')
+ ->label('abuse-key', 'projectId:{projectId},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'deleteReport',
+ description: '/docs/references/advisor/delete-report.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_NOCONTENT,
+ model: Response::MODEL_NONE,
+ ),
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->inject('queueForDeletes')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform,
+ DeleteEvent $queueForDeletes,
+ Event $queueForEvents
+ ): void {
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ if (!$dbForPlatform->deleteDocument('reports', $report->getId())) {
+ throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove report from DB');
+ }
+
+ $queueForDeletes
+ ->setType(DELETE_TYPE_REPORT)
+ ->setDocument($report);
+
+ $queueForEvents
+ ->setParam('reportId', $report->getId())
+ ->setPayload($response->output($report, Response::MODEL_REPORT));
+
+ $response->noContent();
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php
new file mode 100644
index 0000000000..78885a7c5d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php
@@ -0,0 +1,80 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId')
+ ->desc('Get report')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.read')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'getReport',
+ description: '/docs/references/advisor/get-report.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_REPORT,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ $insights = $dbForPlatform->find('insights', [
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ Query::equal('reportInternalId', [$report->getSequence()]),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]);
+
+ $report->setAttribute('insights', $insights);
+
+ $response->dynamic($report, Response::MODEL_REPORT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php
new file mode 100644
index 0000000000..c5debb7f68
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php
@@ -0,0 +1,133 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports')
+ ->desc('List reports')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.read')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'listReports',
+ description: '/docs/references/advisor/list-reports.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_REPORT_LIST,
+ ),
+ ]
+ ))
+ ->param('queries', [], new Reports(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Reports::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ array $queries,
+ bool $includeTotal,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $reportId = $cursor->getValue();
+ $cursorDocument = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($cursorDocument->isEmpty() || $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Report '{$reportId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $reports = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->find('reports', $queries),
+ ['subQueryReportInsights'],
+ );
+ $total = $includeTotal ? $dbForPlatform->count('reports', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
+ if (!empty($reports)) {
+ $reportSequences = \array_map(fn (Document $r) => $r->getSequence(), $reports);
+
+ $insights = $dbForPlatform->find('insights', [
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ Query::equal('reportInternalId', $reportSequences),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]);
+
+ $insightsByReport = [];
+ foreach ($insights as $insight) {
+ $insightsByReport[$insight->getAttribute('reportInternalId')][] = $insight;
+ }
+
+ foreach ($reports as $report) {
+ $report->setAttribute('insights', $insightsByReport[$report->getSequence()] ?? []);
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'reports' => $reports,
+ 'total' => $total,
+ ]), Response::MODEL_REPORT_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Module.php b/src/Appwrite/Platform/Modules/Advisor/Module.php
new file mode 100644
index 0000000000..b28a2421c2
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Module.php
@@ -0,0 +1,14 @@
+addService('http', new Http());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Services/Http.php b/src/Appwrite/Platform/Modules/Advisor/Services/Http.php
new file mode 100644
index 0000000000..2558b00247
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Services/Http.php
@@ -0,0 +1,25 @@
+type = Service::TYPE_HTTP;
+
+ $this->addAction(GetReport::getName(), new GetReport());
+ $this->addAction(ListReports::getName(), new ListReports());
+ $this->addAction(DeleteReport::getName(), new DeleteReport());
+
+ $this->addAction(GetInsight::getName(), new GetInsight());
+ $this->addAction(ListInsights::getName(), new ListInsights());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
index 8100a2c51b..d62782f95e 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
@@ -3,6 +3,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
@@ -421,7 +423,7 @@ abstract class Action extends DatabasesAction
* @param Document[] $documents
* @param Event $queueForEvents
* @param Event $queueForRealtime
- * @param Event $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param Database $dbForProject
* @param EventProcessor $eventProcessor
@@ -434,7 +436,7 @@ abstract class Action extends DatabasesAction
array $documents,
Event $queueForEvents,
Event $queueForRealtime,
- Event $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Event $queueForWebhooks,
Database $dbForProject,
EventProcessor $eventProcessor
@@ -472,9 +474,15 @@ abstract class Action extends DatabasesAction
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
@@ -494,7 +502,6 @@ abstract class Action extends DatabasesAction
$queueForEvents->reset();
$queueForRealtime->reset();
- $queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
index 267a54adb0..2dc3100046 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -80,14 +81,14 @@ class Delete extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -206,7 +207,7 @@ class Delete extends Action
$documents,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
index da3adf1192..393590d1e6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -84,14 +85,14 @@ class Update extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -237,7 +238,7 @@ class Update extends Action
$documents,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
index 5a5ebf48ee..d69298919b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -82,14 +83,14 @@ class Upsert extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -212,7 +213,7 @@ class Upsert extends Action
$upserted,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
index 633a2bbc86..2ade0b2b79 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -137,7 +138,7 @@ class Create extends Action
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
@@ -145,7 +146,7 @@ class Create extends Action
->callback($this->action(...));
}
- public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
+ public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -517,7 +518,7 @@ class Create extends Action
$created,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
index c4d51e6c64..4f91ba3f94 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
@@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -77,7 +79,7 @@ class Update extends Action
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
@@ -97,7 +99,7 @@ class Update extends Action
* @param Event $queueForEvents
* @param Context $usage
* @param Event $queueForRealtime
- * @param Event $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param EventProcessor $eventProcessor
* @return void
@@ -108,7 +110,7 @@ class Update extends Action
* @throws StructureException
* @throws \Utopia\Http\Exception
*/
- public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
+ public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
{
if (!$commit && !$rollback) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
@@ -461,7 +463,15 @@ class Update extends Action
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions->from($queueForEvents)->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
@@ -480,7 +490,6 @@ class Update extends Action
$queueForEvents->reset();
$queueForRealtime->reset();
- $queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
index 09ad9a5741..6b2910aac4 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
@@ -63,7 +63,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
index c723f1bc30..f395d0b490 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
@@ -65,7 +65,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
index d5b62ec903..5acc4626af 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
@@ -65,7 +65,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
index 532ae826e2..2df96958ad 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
@@ -112,7 +112,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
index b4c0c2ffab..97eff24508 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
@@ -60,7 +60,7 @@ class Update extends TransactionsUpdate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
index 37a3db01db..8315a8d04b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
@@ -65,7 +65,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
index bb839b752e..a31ebc15e0 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
@@ -67,7 +67,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
index 364bf4a928..543de8c4bc 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
@@ -67,7 +67,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
index 26649accfb..ea9e3e0b03 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
@@ -109,7 +109,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
index 872927d533..c41186b5c3 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
@@ -61,7 +61,7 @@ class Update extends TransactionsUpdate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
index a4d640b423..4c7d97aa55 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
@@ -63,7 +63,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
index 2784fa220a..18e441ede7 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
@@ -65,7 +65,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
index cfbf6c9158..c26e61d716 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
@@ -65,7 +65,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
index 563b5f60ef..dee8d8e85f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
@@ -106,7 +106,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
index f4bd4d67f5..d6399e6bc0 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
@@ -60,7 +60,7 @@ class Update extends TransactionsUpdate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
index 02dd76294e..f2f51a90e6 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
@@ -5,7 +5,8 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
@@ -95,7 +96,7 @@ class Create extends Base
->inject('user')
->inject('queueForEvents')
->inject('usage')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('geodb')
->inject('store')
->inject('proofForToken')
@@ -123,7 +124,7 @@ class Create extends Base
User $user,
Event $queueForEvents,
Context $usage,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Reader $geodb,
Store $store,
Token $proofForToken,
@@ -294,20 +295,19 @@ class Create extends Base
if ($async) {
if (is_null($scheduledAt)) {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
- $queueForFunctions
- ->setType('http')
- ->setExecution($execution)
- ->setFunction($function)
- ->setBody($body)
- ->setHeaders($headers)
- ->setPath($path)
- ->setMethod($method)
- ->setJWT($jwt)
- ->setProject($project)
- ->setUser($user)
- ->setParam('functionId', $function->getId())
- ->setParam('executionId', $execution->getId())
- ->trigger();
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $project,
+ user: $user,
+ function: $function,
+ functionId: $function->getId(),
+ execution: $execution,
+ type: 'http',
+ jwt: $jwt,
+ body: $body,
+ path: $path,
+ headers: $headers,
+ method: $method,
+ ));
} else {
$data = [
'headers' => $headers,
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
index 00a91141fb..148f0945ac 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
@@ -3,9 +3,10 @@
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Build as BuildPublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Event\Webhook;
@@ -119,7 +120,7 @@ class Create extends Base
->inject('publisherForBuilds')
->inject('queueForRealtime')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('dbForPlatform')
->inject('request')
->inject('gitHub')
@@ -161,7 +162,7 @@ class Create extends Base
BuildPublisher $publisherForBuilds,
Realtime $queueForRealtime,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Database $dbForPlatform,
Request $request,
GitHub $github,
@@ -423,9 +424,15 @@ class Create extends Base
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($ruleCreate)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $ruleCreate->getEvent(),
+ params: $ruleCreate->getParams(),
+ project: $ruleCreate->getProject(),
+ user: $ruleCreate->getUser(),
+ userId: $ruleCreate->getUserId(),
+ payload: $ruleCreate->getPayload(),
+ platform: $ruleCreate->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
index b3fcb2c021..e8713a179d 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
@@ -287,7 +287,29 @@ class Update extends Base
}
// Inform scheduler if function is still active
- $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
+ $schedule = $authorization->skip(fn () => $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')));
+
+ // Re-create schedule if missing
+ if ($schedule->isEmpty()) {
+ $schedule = $authorization->skip(
+ fn () => $dbForPlatform->createDocument('schedules', new Document([
+ 'region' => $project->getAttribute('region'),
+ 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION,
+ 'resourceId' => $function->getId(),
+ 'resourceInternalId' => $function->getSequence(),
+ 'resourceUpdatedAt' => DateTime::now(),
+ 'projectId' => $project->getId(),
+ 'schedule' => $function->getAttribute('schedule'),
+ 'active' => false,
+ ]))
+ );
+
+ $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
+ 'scheduleId' => $schedule->getId(),
+ 'scheduleInternalId' => $schedule->getSequence(),
+ ]));
+ }
+
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
index a0bd37732f..5aa95d3bf2 100644
--- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
+++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
@@ -4,8 +4,9 @@ namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
@@ -63,7 +64,7 @@ class Builds extends Action
->inject('queueForEvents')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('usage')
->inject('publisherForUsage')
@@ -89,7 +90,7 @@ class Builds extends Action
Event $queueForEvents,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Context $usage,
UsagePublisher $publisherForUsage,
@@ -131,7 +132,7 @@ class Builds extends Action
$deviceForFiles,
$publisherForScreenshots,
$queueForWebhooks,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForRealtime,
$queueForEvents,
$usage,
@@ -167,7 +168,7 @@ class Builds extends Action
Device $deviceForFiles,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Context $usage,
@@ -186,11 +187,11 @@ class Builds extends Action
array $platform,
int $timeout
): void {
- Span::add('projectId', $project->getId());
- Span::add('resourceId', $resource->getId());
- Span::add('resourceType', $resource->getCollection());
- Span::add('deploymentId', $deployment->getId());
- Span::add('timeout', $timeout);
+ Span::add('project.id', $project->getId());
+ Span::add('resource.id', $resource->getId());
+ Span::add('resource.type', $resource->getCollection());
+ Span::add('deployment.id', $deployment->getId());
+ Span::add('build.timeout', $timeout);
Console::info('Deployment action started');
@@ -232,12 +233,12 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
- Span::add('runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', ''));
- Span::add('version', $version);
+ Span::add('build.runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', ''));
+ Span::add('build.version', $version);
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
- Span::add('cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
- Span::add('memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT));
+ Span::add('build.cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
+ Span::add('build.memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT));
// Realtime preparation
$event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";
@@ -570,9 +571,15 @@ class Builds extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($deploymentUpdate)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $deploymentUpdate->getEvent(),
+ params: $deploymentUpdate->getParams(),
+ project: $deploymentUpdate->getProject(),
+ user: $deploymentUpdate->getUser(),
+ userId: $deploymentUpdate->getUserId(),
+ payload: $deploymentUpdate->getPayload(),
+ platform: $deploymentUpdate->getPlatform(),
+ ));
/** Trigger Realtime Event */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
index 70d7713280..0429118e41 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
@@ -5,10 +5,10 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
use Appwrite\Event\Database;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
@@ -78,7 +78,7 @@ class Get extends Base
->inject('queueForDeletes')
->inject('publisherForAudits')
->inject('publisherForMails')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
@@ -98,7 +98,7 @@ class Get extends Base
Delete $queueForDeletes,
Audit $publisherForAudits,
MailPublisher $publisherForMails,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
@@ -115,7 +115,7 @@ class Get extends Base
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $publisherForMails,
- System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
+ System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $publisherForFunctions,
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,
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
index 1d10b8d1a0..29c7a7c859 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Functions;
-use Appwrite\Event\Func;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
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('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Func $queueForFunctions, Response $response): void
+ public function action(int|string $threshold, FunctionPublisher $publisherForFunctions, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForFunctions->getSize();
+ $size = $publisherForFunctions->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php
new file mode 100644
index 0000000000..197d82ef58
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php
@@ -0,0 +1,64 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/project')
+ ->httpAlias('/v1/projects/:projectId')
+ ->desc('Get project')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.read')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: null,
+ name: 'get',
+ description: <<inject('response')
+ ->inject('project')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ Response $response,
+ Document $project,
+ ) {
+ if ($project->isEmpty()) {
+ throw new Exception(Exception::PROJECT_NOT_FOUND);
+ }
+
+ $response->dynamic($project, Response::MODEL_PROJECT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php
index 609de96530..d36d0e9005 100644
--- a/src/Appwrite/Platform/Modules/Project/Services/Http.php
+++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
use Appwrite\Platform\Modules\Project\Http\Project\AuthMethods\Update as UpdateAuthMethod;
use Appwrite\Platform\Modules\Project\Http\Project\Delete as DeleteProject;
+use Appwrite\Platform\Modules\Project\Http\Project\Get as GetProject;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Ephemeral\Create as CreateEphemeralKey;
@@ -110,6 +111,7 @@ class Http extends Service
// Project
$this->addAction(DeleteProject::getName(), new DeleteProject());
+ $this->addAction(GetProject::getName(), new GetProject());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
index 363c99dc1f..d2c92fc65c 100644
--- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
+++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
@@ -28,7 +28,6 @@ use Utopia\Pools\Group;
use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
-use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
class Create extends Action
@@ -72,15 +71,6 @@ class Create extends Action
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('teamId', '', new UID(), 'Team unique ID.')
->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true)
- ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true)
- ->param('logo', '', new Text(1024), 'Project logo.', true)
- ->param('url', '', new URL(), 'Project URL.', true)
- ->param('legalName', '', new Text(256), 'Project legal Name. Max length: 256 chars.', true)
- ->param('legalCountry', '', new Text(256), 'Project legal Country. Max length: 256 chars.', true)
- ->param('legalState', '', new Text(256), 'Project legal State. Max length: 256 chars.', true)
- ->param('legalCity', '', new Text(256), 'Project legal City. Max length: 256 chars.', true)
- ->param('legalAddress', '', new Text(256), 'Project legal Address. Max length: 256 chars.', true)
- ->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true)
->inject('request')
->inject('response')
->inject('dbForPlatform')
@@ -90,7 +80,7 @@ class Create extends Action
->callback($this->action(...));
}
- public function action(string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks)
+ public function action(string $projectId, string $name, string $teamId, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks)
{
$team = $dbForPlatform->getDocument('teams', $teamId);
@@ -175,16 +165,7 @@ class Create extends Action
'teamInternalId' => $team->getSequence(),
'teamId' => $team->getId(),
'region' => $region,
- 'description' => $description,
- 'logo' => $logo,
- 'url' => $url,
'version' => APP_VERSION_STABLE,
- 'legalName' => $legalName,
- 'legalCountry' => $legalCountry,
- 'legalState' => $legalState,
- 'legalCity' => $legalCity,
- 'legalAddress' => $legalAddress,
- 'legalTaxId' => ID::custom($legalTaxId),
'services' => new \stdClass(),
'platforms' => null,
'oAuthProviders' => [],
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
index 8bc090bb03..4b48dd49b1 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
@@ -59,9 +59,9 @@ trait Deployment
$resourceType = $repository->getAttribute('resourceType');
$logBase = "vcs.github.event.repo.{$repositoryId}";
- Span::add("{$logBase}.projectId", $projectId);
- Span::add("{$logBase}.resourceId", $resourceId);
- Span::add("{$logBase}.resourceType", $resourceType);
+ Span::add('project.id', $projectId);
+ Span::add("{$logBase}.resource.id", $resourceId);
+ Span::add("{$logBase}.resource.type", $resourceType);
if ($resourceType !== "function" && $resourceType !== "site") {
continue;
diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php
index 7308dc003f..836508c73d 100644
--- a/src/Appwrite/Platform/Tasks/Interval.php
+++ b/src/Appwrite/Platform/Tasks/Interval.php
@@ -101,11 +101,11 @@ class Interval extends Action
]);
$scanned = \count($rules);
- Span::add("interval.domainVerification.scanned", $scanned);
+ Span::add("interval.domain_verification.scanned", $scanned);
if ($scanned === 0) {
- Span::add("interval.domainVerification.processed", 0);
- Span::add("interval.domainVerification.failed", 0);
+ Span::add("interval.domain_verification.processed", 0);
+ Span::add("interval.domain_verification.failed", 0);
return; // No rules to verify
}
@@ -131,7 +131,7 @@ class Interval extends Action
}
}
- Span::add("interval.domainVerification.processed", $processed);
- Span::add("interval.domainVerification.failed", $failed);
+ Span::add("interval.domain_verification.processed", $processed);
+ Span::add("interval.domain_verification.failed", $failed);
}
}
diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php
index fbf965bd00..5d52d905f6 100644
--- a/src/Appwrite/Platform/Tasks/SDKs.php
+++ b/src/Appwrite/Platform/Tasks/SDKs.php
@@ -493,7 +493,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setGitRepo($language['gitUrl'])
->setGitRepoName($language['gitRepoName'])
->setGitUserName($language['gitUserName'])
- ->setLogo($cover)
+ ->setCoverImage($cover)
->setURL('https://appwrite.io')
->setShareText('Appwrite is a backend as a service for building web or mobile apps')
->setShareURL('http://appwrite.io')
diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
index cd7873bab6..49dd851b6d 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Swoole\Coroutine as Co;
use Utopia\Database\Database;
@@ -36,7 +37,10 @@ class ScheduleExecutions extends ScheduleBase
{
$intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds');
- $queueForFunctions = new Func($this->publisherFunctions);
+ $publisherForFunctions = new FunctionPublisher(
+ $this->publisherFunctions,
+ new \Utopia\Queue\Queue(\Utopia\System\System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', \Appwrite\Event\Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', \Appwrite\Event\Event::FUNCTIONS_QUEUE_TTL)
+ );
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
@@ -63,23 +67,22 @@ class ScheduleExecutions extends ScheduleBase
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
- \go(function () use ($queueForFunctions, $schedule, $scheduledAt, $delay, $data, $dbForPlatform) {
+ \go(function () use ($publisherForFunctions, $schedule, $scheduledAt, $delay, $data, $dbForPlatform) {
if ($delay > 0) {
Co::sleep($delay);
}
- $queueForFunctions->setType('schedule')
- // Set functionId instead of function as we don't have $dbForProject
- // TODO: Refactor to use function instead of functionId
- ->setFunctionId($schedule['resource']['resourceId'])
- ->setExecution($schedule['resource'])
- ->setMethod($data['method'] ?? 'POST')
- ->setPath($data['path'] ?? '/')
- ->setHeaders($data['headers'] ?? [])
- ->setBody($data['body'] ?? '')
- ->setProject($schedule['project'])
- ->setUserId($data['userId'] ?? '')
- ->trigger();
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $schedule['project'],
+ userId: $data['userId'] ?? '',
+ functionId: $schedule['resource']['resourceId'],
+ execution: $schedule['resource'],
+ type: 'schedule',
+ body: $data['body'] ?? '',
+ path: $data['path'] ?? '/',
+ headers: $data['headers'] ?? [],
+ method: $data['method'] ?? 'POST',
+ ));
$dbForPlatform->deleteDocument(
'schedules',
diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
index 75908c99c7..c1a1891386 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
@@ -2,13 +2,13 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Cron\CronExpression;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Span\Span;
-use Utopia\System\System;
/**
* ScheduleFunctions
@@ -98,31 +98,29 @@ class ScheduleFunctions extends ScheduleBase
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
- $queueForFunctions = new Func($this->publisherFunctions);
+ $publisherForFunctions = new FunctionPublisher(
+ $this->publisherFunctions,
+ new \Utopia\Queue\Queue(\Utopia\System\System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', \Appwrite\Event\Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', \Appwrite\Event\Event::FUNCTIONS_QUEUE_TTL)
+ );
- $queueForFunctions
- ->setType('schedule')
- ->setFunction($schedule['resource'])
- ->setMethod('POST')
- ->setPath('/')
- ->setProject($schedule['project']);
+ Span::init('schedule.functions.enqueue');
+ try {
+ Span::add('project.id', $schedule['project']->getId());
+ Span::add('function.id', $schedule['resource']->getId());
+ Span::add('schedule.id', $schedule['$id'] ?? '');
- $projectDoc = $schedule['project'];
- $functionDoc = $schedule['resource'];
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
- Span::init('execution.trace.v1_functions_enqueue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $projectDoc->getId());
- Span::add('functionId', $functionDoc->getId());
- Span::add('scheduleId', $schedule['$id'] ?? '');
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $schedule['project'],
+ function: $schedule['resource'],
+ type: 'schedule',
+ method: 'POST',
+ path: '/',
+ ));
+
+ $this->recordEnqueueDelay($delayConfig['nextDate']);
+ } finally {
Span::current()?->finish();
}
-
- $queueForFunctions->trigger();
-
- $this->recordEnqueueDelay($delayConfig['nextDate']);
}
});
}
diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php
index af3d145f85..4a31216599 100644
--- a/src/Appwrite/Platform/Workers/Certificates.php
+++ b/src/Appwrite/Platform/Workers/Certificates.php
@@ -4,9 +4,10 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@@ -54,7 +55,7 @@ class Certificates extends Action
->inject('publisherForMails')
->inject('queueForEvents')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('publisherForCertificates')
->inject('log')
@@ -70,7 +71,7 @@ class Certificates extends Action
* @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $publisherForCertificates
* @param Log $log
@@ -87,7 +88,7 @@ class Certificates extends Action
MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Certificate $publisherForCertificates,
Log $log,
@@ -113,11 +114,11 @@ class Certificates extends Action
switch ($action) {
case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION:
- $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
+ $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
break;
case \Appwrite\Event\Certificate::ACTION_GENERATION:
- $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $publisherForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
+ $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $publisherForMails, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
break;
default:
@@ -130,7 +131,7 @@ class Certificates extends Action
* @param Database $dbForPlatform
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $publisherForCertificates
* @param Log $log
@@ -146,7 +147,7 @@ class Certificates extends Action
Database $dbForPlatform,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Certificate $publisherForCertificates,
Log $log,
@@ -185,7 +186,7 @@ class Certificates extends Action
$rule->setAttribute('logs', $logs);
} finally {
// Update rule and emit events
- $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
+ $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime);
}
// Issue a TLS certificate when domain is verified
@@ -213,7 +214,7 @@ class Certificates extends Action
* @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Log $log
* @param CertificatesAdapter $certificates
@@ -237,7 +238,7 @@ class Certificates extends Action
MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Log $log,
CertificatesAdapter $certificates,
@@ -370,7 +371,7 @@ class Certificates extends Action
// Update rule and emit events
$rule->setAttribute('certificateId', $certificate->getId());
$rule->setAttribute('logs', $logs);
- $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
+ $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime);
}
}
@@ -416,7 +417,7 @@ class Certificates extends Action
* @param Database $dbForPlatform Database connection for console
* @param Event $queueForEvents Event publisher for events
* @param Webhook $queueForWebhooks Webhook publisher for webhooks
- * @param Func $queueForFunctions Function publisher for functions
+ * @param FunctionPublisher $publisherForFunctions Function publisher for functions
* @param Realtime $queueForRealtime Realtime publisher for realtime events
*
* @return void
@@ -426,7 +427,7 @@ class Certificates extends Action
Database $dbForPlatform,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime
): void {
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
@@ -459,9 +460,15 @@ class Certificates extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php
index a5fe352b07..3a865bf6a9 100644
--- a/src/Appwrite/Platform/Workers/Deletes.php
+++ b/src/Appwrite/Platform/Workers/Deletes.php
@@ -216,11 +216,25 @@ class Deletes extends Action
$this->deleteExpiredTransactions($project, $getProjectDB);
$this->deleteOldDeployments($queueForDeletes, $project, $getProjectDB);
break;
+ case DELETE_TYPE_REPORT:
+ $this->deleteReport($dbForPlatform, $project, $document);
+ break;
default:
throw new \Exception('No delete operation for type: ' . \strval($type));
}
}
+ private function deleteReport(Database $dbForPlatform, Document $project, Document $report): void
+ {
+ $projectInternalId = $project->getSequence();
+ $reportInternalId = $report->getSequence();
+
+ $this->deleteByGroup('insights', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::equal('reportInternalId', [$reportInternalId]),
+ ], $dbForPlatform);
+ }
+
private function cleanDatabase(
Document $databaseDoc,
callable $executionActionPerDatabase,
@@ -716,6 +730,26 @@ class Deletes extends Action
Console::error('Failed to delete schedules: ' . $th->getMessage());
}
+ // Delete Advisor insights
+ try {
+ $this->deleteByGroup('insights', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::orderAsc()
+ ], $dbForPlatform);
+ } catch (Throwable $th) {
+ Console::error('Failed to delete insights: ' . $th->getMessage());
+ }
+
+ // Delete Advisor reports
+ try {
+ $this->deleteByGroup('reports', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::orderAsc()
+ ], $dbForPlatform);
+ } catch (Throwable $th) {
+ Console::error('Failed to delete reports: ' . $th->getMessage());
+ }
+
/**
* @var Database $dbForProject
*/
diff --git a/src/Appwrite/Platform/Workers/Executions.php b/src/Appwrite/Platform/Workers/Executions.php
index 404b04ce76..8dbf10cae6 100644
--- a/src/Appwrite/Platform/Workers/Executions.php
+++ b/src/Appwrite/Platform/Workers/Executions.php
@@ -8,7 +8,6 @@ use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
-use Utopia\System\System;
class Executions extends Action
{
@@ -41,19 +40,11 @@ class Executions extends Action
throw new Exception('Missing execution');
}
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- $resourceId = $execution->getAttribute('resourceId', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
- Span::init('execution.trace.executions_worker_upsert');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $executionMessage->project->getId());
- Span::add('functionId', $resourceId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('resourceType', $execution->getAttribute('resourceType', ''));
- Span::current()?->finish();
- }
+ Span::add('project.id', $executionMessage->project->getId());
+ Span::add('function.id', $execution->getAttribute('resourceId', ''));
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('resource.type', $execution->getAttribute('resourceType', ''));
$dbForProject->upsertDocument('executions', $execution);
}
diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php
index a72b16cc23..73c1db9444 100644
--- a/src/Appwrite/Platform/Workers/Functions.php
+++ b/src/Appwrite/Platform/Workers/Functions.php
@@ -5,7 +5,8 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
@@ -46,7 +47,7 @@ class Functions extends Action
->inject('message')
->inject('dbForProject')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('queueForEvents')
->inject('bus')
@@ -61,7 +62,7 @@ class Functions extends Action
Message $message,
Database $dbForProject,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Bus $bus,
@@ -78,20 +79,27 @@ class Functions extends Action
);
}
- $type = $payload['type'] ?? '';
+ $functionMessage = FunctionMessage::fromArray($payload);
+ $type = $functionMessage->type;
- $events = $payload['events'] ?? [];
- $data = $payload['body'] ?? '';
- $eventData = $payload['payload'] ?? '';
- $platform = $payload['platform'] ?? Config::getParam('platform', []);
- $function = new Document($payload['function'] ?? []);
- $functionId = $payload['functionId'] ?? '';
- $user = new Document($payload['user'] ?? []);
- $userId = $payload['userId'] ?? '';
- $method = $payload['method'] ?? 'POST';
- $headers = $payload['headers'] ?? [];
- $path = $payload['path'] ?? '/';
- $jwt = $payload['jwt'] ?? '';
+ Span::add('project.id', $project->getId());
+ Span::add('payload.type', $type);
+ Span::add('queue.pid', $message->getPid());
+ Span::add('queue.name', $message->getQueue());
+ Span::add('message.timestamp', (string) $message->getTimestamp());
+
+ $events = $functionMessage->events;
+ $data = $functionMessage->body;
+ $eventData = $functionMessage->payload;
+ $platform = !empty($functionMessage->platform) ? $functionMessage->platform : Config::getParam('platform', []);
+ $function = $functionMessage->function ?? new Document();
+ $functionId = $functionMessage->functionId ?? '';
+ $user = $functionMessage->user ?? new Document();
+ $userId = $functionMessage->userId ?? '';
+ $method = $functionMessage->method ?: 'POST';
+ $headers = $functionMessage->headers;
+ $path = $functionMessage->path ?: '/';
+ $jwt = $functionMessage->jwt;
if ($user->isEmpty() && !empty($userId)) {
$user = $dbForProject->getDocument('users', $userId);
@@ -118,19 +126,7 @@ class Functions extends Action
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_dequeue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $function->getId());
- Span::add('payloadType', $type);
- Span::add('queuePid', $message->getPid());
- Span::add('queueName', $message->getQueue());
- Span::add('messageTimestamp', (string) $message->getTimestamp());
- Span::current()?->finish();
- }
+ Span::add('function.id', $function->getId());
}
if (!empty($events)) {
@@ -172,7 +168,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -191,7 +187,7 @@ class Functions extends Action
user: $user,
jwt: null,
event: $events[0],
- eventData: \is_string($eventData) ? $eventData : \json_encode($eventData),
+ eventData: \json_encode($eventData) ?: null,
executionId: null,
);
Console::success('Triggered function: ' . $events[0]);
@@ -216,7 +212,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -242,7 +238,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -322,19 +318,11 @@ class Functions extends Action
'duration' => 0.0,
]);
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $function->getId());
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('trigger', $trigger);
- Span::add('status', $execution->getAttribute('status', ''));
- Span::current()?->finish();
- }
+ Span::add('function.id', $function->getId());
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('execution.trigger', $trigger);
+ Span::add('execution.status', $execution->getAttribute('status', ''));
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
@@ -345,7 +333,7 @@ class Functions extends Action
/**
* @param Log $log
* @param Database $dbForProject
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Event $queueForEvents
* @param Document $project
@@ -367,7 +355,7 @@ class Functions extends Action
Log $log,
Database $dbForProject,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Bus $bus,
@@ -391,6 +379,10 @@ class Functions extends Action
$deploymentId = $function->getAttribute('deploymentId', '');
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
+ Span::add('function.id', $functionId);
+ Span::add('deployment.id', $deploymentId);
+ Span::add('execution.trigger', $trigger);
+
$log->addTag('deploymentId', $deploymentId);
/** Check if deployment exists */
@@ -450,6 +442,8 @@ class Functions extends Action
}
$headers['x-appwrite-execution-id'] = $executionId;
+ Span::add('execution.id', $executionId);
+
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
@@ -554,18 +548,6 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_executor');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $functionId);
- Span::add('executionId', $executionId);
- Span::add('deploymentId', $deployment->getId());
- Span::add('trigger', $trigger);
- Span::current()?->finish();
- }
try {
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
@@ -642,19 +624,8 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_execution_completed_bus');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $functionId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('status', $execution->getAttribute('status', ''));
- Span::add('trigger', $trigger);
- Span::current()?->finish();
- }
+ Span::add('execution.status', $execution->getAttribute('status', ''));
+
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -680,9 +651,15 @@ class Functions extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php
index 03adebc4b5..7af079e914 100644
--- a/src/Appwrite/Platform/Workers/Messaging.php
+++ b/src/Appwrite/Platform/Workers/Messaging.php
@@ -106,27 +106,20 @@ class Messaging extends Action
Span::add('message.type', $type);
- try {
- switch ($type) {
- case MESSAGE_SEND_TYPE_INTERNAL:
- $message = new Document($payload['message'] ?? []);
- $recipients = $payload['recipients'] ?? [];
+ switch ($type) {
+ case MESSAGE_SEND_TYPE_INTERNAL:
+ $message = new Document($payload['message'] ?? []);
+ $recipients = $payload['recipients'] ?? [];
- $this->sendInternalSMSMessage($message, $project, $recipients, $log);
- break;
- case MESSAGE_SEND_TYPE_EXTERNAL:
- $message = $dbForProject->getDocument('messages', $payload['messageId']);
+ $this->sendInternalSMSMessage($message, $project, $recipients, $log);
+ break;
+ case MESSAGE_SEND_TYPE_EXTERNAL:
+ $message = $dbForProject->getDocument('messages', $payload['messageId']);
- $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $publisherForUsage);
- break;
- default:
- throw new \Exception('Unknown message type: ' . $type);
- }
- } catch (\Throwable $e) {
- Span::error($e);
- throw $e;
- } finally {
- Span::current()?->finish();
+ $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $publisherForUsage);
+ break;
+ default:
+ throw new \Exception('Unknown message type: ' . $type);
}
}
@@ -162,21 +155,26 @@ class Messaging extends Action
}
if (\count($userIds) > 0) {
- $users = $dbForProject->find('users', [
- Query::equal('$id', $userIds),
- Query::limit(\count($userIds)),
- ]);
- foreach ($users as $user) {
- $targets = \array_filter($user->getAttribute('targets'), function (Document $target) use ($providerType) {
- return $target->getAttribute('providerType') === $providerType;
- });
+ $limit = 1000;
+ $offset = 0;
+
+ do {
+ $targets = $dbForProject->find('targets', [
+ Query::equal('userId', $userIds),
+ Query::select(['providerId', 'identifier']),
+ Query::equal('providerType', [$providerType]),
+ Query::limit($limit),
+ Query::offset($offset),
+ ]);
\array_push($allTargets, ...$targets);
- }
+ $offset += \count($targets);
+ } while (\count($targets) === $limit);
}
if (\count($targetIds) > 0) {
$targets = $dbForProject->find('targets', [
+ Query::select(['providerId', 'identifier']),
Query::equal('$id', $targetIds),
Query::equal('providerType', [$providerType]),
Query::limit(\count($targetIds)),
diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php
index 9c06fbda2f..03c901ead6 100644
--- a/src/Appwrite/Promises/Swoole.php
+++ b/src/Appwrite/Promises/Swoole.php
@@ -8,7 +8,7 @@ use Utopia\DI\Container;
class Swoole extends Promise
{
- private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
+ private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia__';
public function __construct(?callable $executor = null)
{
diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php
index 4c7fa6f377..fc67dedb13 100644
--- a/src/Appwrite/SDK/Specification/Format.php
+++ b/src/Appwrite/SDK/Specification/Format.php
@@ -757,28 +757,28 @@ abstract class Format
case 'updateAuthMethod':
switch ($param) {
case 'methodId':
- return 'AuthMethod';
+ return 'ProjectAuthMethodId';
}
break;
case 'getPolicy':
switch ($param) {
case 'policyId':
- return 'ProjectPolicy';
+ return 'ProjectPolicyId';
}
break;
case 'getOAuth2Provider':
switch ($param) {
case 'providerId':
- return 'OAuthProvider';
+ return 'ProjectOAuthProviderId';
}
break;
case 'getEmailTemplate':
case 'updateEmailTemplate':
switch ($param) {
case 'templateId':
- return 'EmailTemplateType';
+ return 'ProjectEmailTemplateId';
case 'locale':
- return 'EmailTemplateLocale';
+ return 'ProjectEmailTemplateLocale';
}
break;
case 'getUsage':
@@ -787,6 +787,39 @@ abstract class Format
return 'ProjectUsageRange';
}
break;
+ case 'updateProtocol':
+ switch ($param) {
+ case 'protocolId':
+ return 'ProjectProtocolId';
+ }
+ break;
+ case 'updateService':
+ switch ($param) {
+ case 'serviceId':
+ return 'ProjectServiceId';
+ }
+ break;
+ case 'updateSMTP':
+ case 'createSMTPTest':
+ switch ($param) {
+ case 'secure':
+ return 'ProjectSMTPSecure';
+ }
+ break;
+ case 'updateOAuth2Google':
+ switch ($param) {
+ case 'prompt':
+ return 'ProjectOAuth2GooglePrompt';
+ }
+ break;
+ case 'createKey':
+ case 'createEphemeralKey':
+ case 'updateKey':
+ switch ($param) {
+ case 'scopes':
+ return 'ProjectKeyScopes';
+ }
+ break;
}
break;
case 'projects':
@@ -1019,8 +1052,12 @@ abstract class Format
return self::REQUEST_PARAMETER_OVERRIDES;
}
- public function getResponseEnumName(string $model, string $param): ?string
+ public function getResponseEnumName(string $model, string $param, ?string $enumSDKName = null): ?string
{
+ if ($enumSDKName) {
+ return $enumSDKName;
+ }
+
if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') {
return 'PlatformType';
}
diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
index f69ec3972a..3be3fe7115 100644
--- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
+++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
@@ -988,13 +988,13 @@ class OpenAPI3 extends Format
if ($rule['type'] === 'enum' && !empty($rule['enum'])) {
if ($rule['array']) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName;
}
} else {
$output['components']['schemas'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName;
}
diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php
index 52e33bcc27..9a16bc8bbe 100644
--- a/src/Appwrite/SDK/Specification/Format/Swagger2.php
+++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php
@@ -975,13 +975,13 @@ class Swagger2 extends Format
if ($rule['type'] === 'enum' && !empty($rule['enum'])) {
if ($rule['array']) {
$output['definitions'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['definitions'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName;
}
} else {
$output['definitions'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['definitions'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName;
}
diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Insights.php b/src/Appwrite/Utopia/Database/Validator/Queries/Insights.php
new file mode 100644
index 0000000000..18badf8722
--- /dev/null
+++ b/src/Appwrite/Utopia/Database/Validator/Queries/Insights.php
@@ -0,0 +1,24 @@
+stripProjectMetadata($content);
+ break;
+ }
+
+ return $content;
+ }
+
+ protected function stripProjectMetadata(array $content): array
+ {
+ unset(
+ $content['description'],
+ $content['logo'],
+ $content['url'],
+ $content['legalName'],
+ $content['legalCountry'],
+ $content['legalState'],
+ $content['legalCity'],
+ $content['legalAddress'],
+ $content['legalTaxId'],
+ );
+
+ return $content;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php
index e02e70b3d8..c8dcb5f46a 100644
--- a/src/Appwrite/Utopia/Response.php
+++ b/src/Appwrite/Utopia/Response.php
@@ -249,6 +249,9 @@ class Response extends SwooleResponse
// Project
public const MODEL_PROJECT = 'project';
public const MODEL_PROJECT_LIST = 'projectList';
+ public const MODEL_PROJECT_AUTH_METHOD = 'projectAuthMethod';
+ public const MODEL_PROJECT_SERVICE = 'projectService';
+ public const MODEL_PROJECT_PROTOCOL = 'projectProtocol';
public const MODEL_WEBHOOK = 'webhook';
public const MODEL_WEBHOOK_LIST = 'webhookList';
public const MODEL_KEY = 'key';
@@ -332,6 +335,13 @@ class Response extends SwooleResponse
public const MODEL_HEALTH_CERTIFICATE = 'healthCertificate';
public const MODEL_HEALTH_STATUS_LIST = 'healthStatusList';
+ // Advisor
+ public const MODEL_INSIGHT = 'insight';
+ public const MODEL_INSIGHT_LIST = 'insightList';
+ public const MODEL_INSIGHT_CTA = 'insightCTA';
+ public const MODEL_REPORT = 'report';
+ public const MODEL_REPORT_LIST = 'reportList';
+
// Console
public const MODEL_CONSOLE_VARIABLES = 'consoleVariables';
public const MODEL_CONSOLE_OAUTH2_PROVIDER_PARAMETER = 'consoleOAuth2ProviderParameter';
@@ -435,9 +445,10 @@ class Response extends SwooleResponse
return isset(self::$models[$key]);
}
- public function applyFilters(array $data, string $model): array
+ public function applyFilters(array $data, string $model, Document $raw): array
{
foreach ($this->filters as $filter) {
+ $filter->setRawContent($raw);
$data = $filter->parse($data, $model);
}
@@ -457,7 +468,7 @@ class Response extends SwooleResponse
public function dynamic(Document $document, string $model): void
{
$output = $this->output(clone $document, $model);
- $output = $this->applyFilters($output, $model);
+ $output = $this->applyFilters($output, $model, raw: clone $document);
switch ($this->getContentType()) {
case self::CONTENT_TYPE_JSON:
diff --git a/src/Appwrite/Utopia/Response/Filter.php b/src/Appwrite/Utopia/Response/Filter.php
index bd82467f81..13833be328 100644
--- a/src/Appwrite/Utopia/Response/Filter.php
+++ b/src/Appwrite/Utopia/Response/Filter.php
@@ -2,8 +2,15 @@
namespace Appwrite\Utopia\Response;
+use Utopia\Database\Document;
+
abstract class Filter
{
+ /**
+ * @var ?Document $rawContent
+ */
+ protected ?Document $rawContent = null;
+
/**
* Parse the content to another format.
*
@@ -14,6 +21,10 @@ abstract class Filter
*/
abstract public function parse(array $content, string $model): array;
+ public function setRawContent(Document $rawContent): void
+ {
+ $this->rawContent = $rawContent;
+ }
/**
* Handle list
diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php
new file mode 100644
index 0000000000..3867ba907f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Filters/V26.php
@@ -0,0 +1,227 @@
+ $this->parseProject($content, $this->rawContent),
+ Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', function ($item) {
+ $projectId = $item['$id'] ?? '';
+
+ $rawProjects = $this->rawContent->getAttribute('projects', []);
+ $rawProject = new Document();
+ foreach ($rawProjects as $rawItem) {
+ if ($rawItem->getId() === $projectId) {
+ $rawProject = $rawItem;
+ break;
+ }
+ }
+
+ return $this->parseProject($item, $rawProject);
+ }),
+ default => $content,
+ };
+ }
+
+ private function parseProject(array $content, Document $raw): array
+ {
+ $this->expandAuthMethods($content);
+ $this->expandServices($content);
+ $this->expandProtocols($content);
+
+ unset($content['authMethods'], $content['services'], $content['protocols']);
+
+ $auths = new Document($raw->getAttribute('auths', []));
+ $content['authLimit'] = $auths->getAttribute('limit', 0);
+ $content['authDuration'] = $auths->getAttribute('duration', TOKEN_EXPIRATION_LOGIN_LONG);
+ $content['authSessionsLimit'] = $auths->getAttribute('maxSessions', 0);
+ $content['authPasswordHistory'] = $auths->getAttribute('passwordHistory', 0);
+ $content['authPasswordDictionary'] = $auths->getAttribute('passwordDictionary', false);
+ $content['authPersonalDataCheck'] = $auths->getAttribute('personalDataCheck', false);
+ $content['authDisposableEmails'] = $auths->getAttribute('disposableEmails', false);
+ $content['authCanonicalEmails'] = $auths->getAttribute('canonicalEmails', false);
+ $content['authFreeEmails'] = $auths->getAttribute('freeEmails', false);
+ $content['authMockNumbers'] = $auths->getAttribute('mockNumbers', []);
+ $content['authSessionAlerts'] = $auths->getAttribute('sessionAlerts', false);
+ $content['authMembershipsUserName'] = $auths->getAttribute('membershipsUserName', false);
+ $content['authMembershipsUserEmail'] = $auths->getAttribute('membershipsUserEmail', false);
+ $content['authMembershipsMfa'] = $auths->getAttribute('membershipsMfa', false);
+ $content['authMembershipsUserId'] = $auths->getAttribute('membershipsUserId', false);
+ $content['authMembershipsUserPhone'] = $auths->getAttribute('membershipsUserPhone', false);
+ $content['authInvalidateSessions'] = $auths->getAttribute('invalidateSessions', false);
+
+ $content['description'] = $raw->getAttribute('description', '');
+ $content['logo'] = $raw->getAttribute('logo', '');
+ $content['url'] = $raw->getAttribute('url', '');
+ $content['legalName'] = $raw->getAttribute('legalName', '');
+ $content['legalCountry'] = $raw->getAttribute('legalCountry', '');
+ $content['legalState'] = $raw->getAttribute('legalState', '');
+ $content['legalCity'] = $raw->getAttribute('legalCity', '');
+ $content['legalAddress'] = $raw->getAttribute('legalAddress', '');
+ $content['legalTaxId'] = $raw->getAttribute('legalTaxId', '');
+
+ $content['oAuthProviders'] = $this->expandOAuthProviders($raw);
+
+ $content['platforms'] = [];
+ foreach ($raw->getAttribute('platforms', []) as $platform) {
+ $content['platforms'][] = $this->parsePlatform($platform);
+ }
+
+ $content['webhooks'] = [];
+ foreach ($raw->getAttribute('webhooks', []) as $webhook) {
+ $content['webhooks'][] = $this->parseWebhook($webhook);
+ }
+
+ $content['keys'] = [];
+ foreach ($raw->getAttribute('keys', []) as $key) {
+ $content['keys'][] = $this->parseKey($key);
+ }
+
+ return $content;
+ }
+
+ private function parsePlatform(Document $platform): array
+ {
+ $type = $platform->getAttribute('type', '');
+ $key = $platform->getAttribute('key', '');
+
+ $result = [
+ '$id' => $platform->getAttribute('$id', ''),
+ '$createdAt' => $platform->getAttribute('$createdAt', ''),
+ '$updatedAt' => $platform->getAttribute('$updatedAt', ''),
+ 'name' => $platform->getAttribute('name', ''),
+ 'type' => $type,
+ ];
+
+ switch ($type) {
+ case Platform::TYPE_ANDROID:
+ $result['applicationId'] = $key;
+ break;
+ case Platform::TYPE_APPLE:
+ $result['bundleIdentifier'] = $key;
+ break;
+ case Platform::TYPE_LINUX:
+ $result['packageName'] = $key;
+ break;
+ case Platform::TYPE_WINDOWS:
+ $result['packageIdentifierName'] = $key;
+ break;
+ default:
+ // Web and backwards-compatibility types are mapped to web
+ $result['hostname'] = $platform->getAttribute('hostname', '');
+ $result['key'] = $key;
+ break;
+ }
+
+ return $result;
+ }
+
+ private function parseWebhook(Document $webhook): array
+ {
+ return [
+ '$id' => $webhook->getAttribute('$id', ''),
+ '$createdAt' => $webhook->getAttribute('$createdAt', ''),
+ '$updatedAt' => $webhook->getAttribute('$updatedAt', ''),
+ 'name' => $webhook->getAttribute('name', ''),
+ 'url' => $webhook->getAttribute('url', ''),
+ 'events' => $webhook->getAttribute('events', []),
+ 'tls' => $webhook->getAttribute('security', true),
+ 'authUsername' => $webhook->getAttribute('httpUser', ''),
+ 'authPassword' => $webhook->getAttribute('httpPass', ''),
+ 'secret' => $webhook->getAttribute('signatureKey', ''),
+ 'enabled' => $webhook->getAttribute('enabled', true),
+ 'logs' => $webhook->getAttribute('logs', ''),
+ 'attempts' => $webhook->getAttribute('attempts', 0),
+ ];
+ }
+
+ private function parseKey(Document $key): array
+ {
+ return [
+ '$id' => $key->getAttribute('$id', ''),
+ '$createdAt' => $key->getAttribute('$createdAt', ''),
+ '$updatedAt' => $key->getAttribute('$updatedAt', ''),
+ 'name' => $key->getAttribute('name', ''),
+ 'expire' => $key->getAttribute('expire', ''),
+ 'scopes' => $key->getAttribute('scopes', []),
+ 'secret' => $key->getAttribute('secret', ''),
+ 'accessedAt' => $key->getAttribute('accessedAt', ''),
+ 'sdks' => $key->getAttribute('sdks', []),
+ ];
+ }
+
+ private function expandAuthMethods(array &$content): void
+ {
+ $authMethods = [];
+ foreach ($content['authMethods'] ?? [] as $method) {
+ $authMethods[$method['$id'] ?? ''] = $method['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('auth', []) as $id => $method) {
+ $key = $method['key'] ?? '';
+ $content['auth' . ucfirst($key)] = $authMethods[$id] ?? true;
+ }
+ }
+
+ private function expandServices(array &$content): void
+ {
+ $services = [];
+ foreach ($content['services'] ?? [] as $service) {
+ $services[$service['$id'] ?? ''] = $service['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('services', []) as $id => $service) {
+ if (!($service['optional'] ?? false)) {
+ continue;
+ }
+ $key = $service['key'] ?? '';
+ $content['serviceStatusFor' . ucfirst($key)] = $services[$id] ?? true;
+ }
+ }
+
+ private function expandProtocols(array &$content): void
+ {
+ $protocols = [];
+ foreach ($content['protocols'] ?? [] as $protocol) {
+ $protocols[$protocol['$id'] ?? ''] = $protocol['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('protocols', []) as $id => $api) {
+ $key = $api['key'] ?? '';
+ $content['protocolStatusFor' . ucfirst($key)] = $protocols[$id] ?? true;
+ }
+ }
+
+ private function expandOAuthProviders(Document $raw): array
+ {
+ $providers = Config::getParam('oAuthProviders', []);
+ $providerValues = $raw->getAttribute('oAuthProviders', []);
+ $projectProviders = [];
+
+ foreach ($providers as $key => $provider) {
+ if (!($provider['enabled'] ?? false)) {
+ continue;
+ }
+
+ $projectProviders[] = [
+ 'key' => $key,
+ 'name' => $provider['name'] ?? '',
+ 'appId' => $providerValues[$key . 'Appid'] ?? '',
+ 'secret' => '',
+ 'enabled' => $providerValues[$key . 'Enabled'] ?? false,
+ ];
+ }
+
+ return $projectProviders;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Insight.php b/src/Appwrite/Utopia/Response/Model/Insight.php
new file mode 100644
index 0000000000..39a12a9832
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Insight.php
@@ -0,0 +1,130 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Insight creation date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Insight update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('reportId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Parent report ID. Insights always belong to a report.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight type. One of databaseIndex (legacy), tablesDBIndex, documentsDBIndex, vectorsDBIndex, databasePerformance, sitePerformance, siteAccessibility, siteSeo, functionPerformance. The index types are engine-specific so each CTA can pair the right service+method (databases.createIndex, tablesDB.createIndex, documentsDB.createIndex, or vectorsDB.createIndex).',
+ 'default' => '',
+ 'example' => 'tablesDBIndex',
+ ])
+ ->addRule('severity', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight severity. One of info, warning, critical.',
+ 'default' => 'info',
+ 'example' => 'warning',
+ ])
+ ->addRule('status', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight status. One of active, dismissed.',
+ 'default' => 'active',
+ 'example' => 'active',
+ ])
+ ->addRule('resourceType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Type of the resource the insight is about. Plural noun, e.g. databases, sites, functions.',
+ 'default' => '',
+ 'example' => 'databases',
+ ])
+ ->addRule('resourceId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the resource the insight is about.',
+ 'default' => '',
+ 'example' => 'main',
+ ])
+ ->addRule('parentResourceType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Plural noun for the parent resource that contains the insight\'s resource, e.g. an insight about a column index on a table → resourceType=indexes, parentResourceType=tables. Empty when the resource has no parent.',
+ 'default' => '',
+ 'example' => 'tables',
+ ])
+ ->addRule('parentResourceId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the parent resource. Empty when the resource has no parent.',
+ 'default' => '',
+ 'example' => 'orders',
+ ])
+ ->addRule('title', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight title.',
+ 'default' => '',
+ 'example' => 'Missing index on collection orders',
+ ])
+ ->addRule('summary', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Short markdown summary describing the insight.',
+ 'default' => '',
+ 'example' => 'Queries against `orders.status` are scanning the full collection.',
+ ])
+ ->addRule('ctas', [
+ 'type' => Response::MODEL_INSIGHT_CTA,
+ 'description' => 'List of call-to-action buttons attached to this insight.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true,
+ ])
+ ->addRule('analyzedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the insight was analyzed in ISO 8601 format.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ])
+ ->addRule('dismissedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the insight was dismissed in ISO 8601 format. Empty when not dismissed.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ])
+ ->addRule('dismissedBy', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'User ID that dismissed the insight. Empty when not dismissed.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ 'required' => false,
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'Insight';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_INSIGHT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/InsightCTA.php b/src/Appwrite/Utopia/Response/Model/InsightCTA.php
new file mode 100644
index 0000000000..3ebd8b5796
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/InsightCTA.php
@@ -0,0 +1,48 @@
+addRule('label', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Human-readable label for the CTA, used in UI.',
+ 'default' => '',
+ 'example' => 'Create missing index',
+ ])
+ ->addRule('service', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Public API service (SDK namespace) the client should invoke. Must match the engine that owns the resource — for index suggestions: databases (legacy), tablesDB, documentsDB, or vectorsDB.',
+ 'default' => '',
+ 'example' => 'tablesDB',
+ ])
+ ->addRule('method', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Public API method on the chosen service the client should invoke when this CTA is triggered.',
+ 'default' => '',
+ 'example' => 'createIndex',
+ ])
+ ->addRule('params', [
+ 'type' => self::TYPE_JSON,
+ 'description' => 'Parameter map the client should pass to the service method when this CTA is triggered. Keys match the target API\'s parameter names (e.g. databaseId/tableId/columns for tablesDB, databaseId/collectionId/attributes for the legacy Databases API).',
+ 'default' => new \stdClass(),
+ 'example' => ['databaseId' => 'main', 'tableId' => 'orders', 'key' => '_idx_status', 'type' => 'key', 'columns' => ['status']],
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'InsightCTA';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_INSIGHT_CTA;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php
index 36be3b751f..af2a21d551 100644
--- a/src/Appwrite/Utopia/Response/Model/Project.php
+++ b/src/Appwrite/Utopia/Response/Model/Project.php
@@ -12,6 +12,7 @@ class Project extends Model
public function __construct()
{
$this
+ // Basic project information
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Project ID.',
@@ -36,210 +37,23 @@ class Project extends Model
'default' => '',
'example' => 'New Project',
])
- ->addRule('description', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project description.',
- 'default' => '',
- 'example' => 'This is a new project.',
- ])
->addRule('teamId', [
'type' => self::TYPE_STRING,
'description' => 'Project team ID.',
'default' => '',
'example' => '1592981250',
])
- ->addRule('logo', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project logo file ID.',
- 'default' => '',
- 'example' => '5f5c451b403cb',
- ])
- ->addRule('url', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project website URL.',
- 'default' => '',
- 'example' => '5f5c451b403cb',
- ])
- ->addRule('legalName', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company legal name.',
- 'default' => '',
- 'example' => 'Company LTD.',
- ])
- ->addRule('legalCountry', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format.',
- 'default' => '',
- 'example' => 'US',
- ])
- ->addRule('legalState', [
- 'type' => self::TYPE_STRING,
- 'description' => 'State name.',
- 'default' => '',
- 'example' => 'New York',
- ])
- ->addRule('legalCity', [
- 'type' => self::TYPE_STRING,
- 'description' => 'City name.',
- 'default' => '',
- 'example' => 'New York City.',
- ])
- ->addRule('legalAddress', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company Address.',
- 'default' => '',
- 'example' => '620 Eighth Avenue, New York, NY 10018',
- ])
- ->addRule('legalTaxId', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company Tax ID.',
- 'default' => '',
- 'example' => '131102020',
- ])
- ->addRule('authDuration', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Session duration in seconds.',
- 'default' => TOKEN_EXPIRATION_LOGIN_LONG,
- 'example' => 60,
- ])
- ->addRule('authLimit', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max users allowed. 0 is unlimited.',
- 'default' => 0,
- 'example' => 100,
- ])
- ->addRule('authSessionsLimit', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max sessions allowed per user. 100 maximum.',
- 'default' => 10,
- 'example' => 10,
- ])
- ->addRule('authPasswordHistory', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max allowed passwords in the history list per user. Max passwords limit allowed in history is 20. Use 0 for disabling password history.',
- 'default' => 0,
- 'example' => 5,
- ])
- ->addRule('authPasswordDictionary', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authPersonalDataCheck', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to check the user password for similarity with their personal data.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authDisposableEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authCanonicalEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to require canonical email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authFreeEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to disallow free email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMockNumbers', [
- 'type' => Response::MODEL_MOCK_NUMBER,
- 'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).',
- 'default' => [],
- 'array' => true,
- 'example' => [new \stdClass()],
- ])
- ->addRule('authSessionAlerts', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to send session alert emails to users.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserName', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user names in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserEmail', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user emails in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsMfa', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user MFA status in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserId', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user IDs in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserPhone', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user phone numbers in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authInvalidateSessions', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not all existing sessions should be invalidated on password change',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('oAuthProviders', [
- 'type' => Response::MODEL_AUTH_PROVIDER,
- 'description' => 'List of Auth Providers.',
- 'default' => [],
- 'example' => [new \stdClass()],
- 'array' => true,
- ])
- ->addRule('platforms', [
- 'type' => [
- Response::MODEL_PLATFORM_WEB,
- Response::MODEL_PLATFORM_APPLE,
- Response::MODEL_PLATFORM_ANDROID,
- Response::MODEL_PLATFORM_WINDOWS,
- Response::MODEL_PLATFORM_LINUX,
- ],
- 'description' => 'List of Platforms.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
- ->addRule('webhooks', [
- 'type' => Response::MODEL_WEBHOOK,
- 'description' => 'List of Webhooks.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
- ->addRule('keys', [
- 'type' => Response::MODEL_KEY,
- 'description' => 'List of API Keys.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
+
+ // Resource: Dev Keys
->addRule('devKeys', [
'type' => Response::MODEL_DEV_KEY,
- 'description' => 'List of dev keys.',
+ 'description' => 'Deprecated since 1.9.5: List of dev keys.',
'default' => [],
'example' => new \stdClass(),
'array' => true,
])
+
+ // Resource: SMTP
->addRule('smtpEnabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Status for custom SMTP',
@@ -301,6 +115,8 @@ class Project extends Model
'default' => '',
'example' => 'tls',
])
+
+ // Resource: Ping
->addRule('pingCount', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of times the ping was received for this project.',
@@ -313,6 +129,8 @@ class Project extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
+
+ // Resource: Labels
->addRule('labels', [
'type' => self::TYPE_STRING,
'description' => 'Labels for the project.',
@@ -320,64 +138,42 @@ class Project extends Model
'example' => ['vip'],
'array' => true,
])
+
+ // Resource: Billing
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Project status.',
'default' => 'active',
'example' => 'active',
])
+
+ // Resource: Auth methods
+ ->addRule('authMethods', [
+ 'type' => Response::MODEL_PROJECT_AUTH_METHOD,
+ 'description' => 'List of auth methods.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
+
+ // Resource: Services
+ ->addRule('services', [
+ 'type' => Response::MODEL_PROJECT_SERVICE,
+ 'description' => 'List of services.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
+
+ // Resource: Protocols
+ ->addRule('protocols', [
+ 'type' => Response::MODEL_PROJECT_PROTOCOL,
+ 'description' => 'List of protocols.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
;
-
- $services = Config::getParam('services', []);
- $auth = Config::getParam('auth', []);
-
- foreach ($auth as $index => $method) {
- $name = $method['name'] ?? '';
- $key = $method['key'] ?? '';
-
- $this
- ->addRule('auth' . ucfirst($key), [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => $name . ' auth method status',
- 'example' => true,
- 'default' => true,
- ])
- ;
- }
-
- foreach ($services as $service) {
- if (!$service['optional']) {
- continue;
- }
-
- $name = $service['name'] ?? '';
- $key = $service['key'] ?? '';
-
- $this
- ->addRule('serviceStatusFor' . ucfirst($key), [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => $name . ' service status',
- 'example' => true,
- 'default' => true,
- ])
- ;
- }
-
- $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,
- ])
- ;
- }
}
/**
@@ -408,10 +204,9 @@ class Project extends Model
public function filter(Document $document): Document
{
$this->expandSmtpFields($document);
- $this->expandServiceFields($document);
- $this->expandApiFields($document);
- $this->expandAuthFields($document);
- $this->expandOAuthProviders($document);
+ $this->expandServices($document);
+ $this->expandProtocols($document);
+ $this->expandAuthMethods($document);
return $document;
}
@@ -422,8 +217,8 @@ class Project extends Model
return;
}
- // SMTP
$smtp = $document->getAttribute('smtp', []);
+
$document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false);
$document->setAttribute('smtpSenderEmail', $smtp['senderEmail'] ?? '');
$document->setAttribute('smtpSenderName', $smtp['senderName'] ?? '');
@@ -436,100 +231,52 @@ class Project extends Model
$document->setAttribute('smtpSecure', $smtp['secure'] ?? '');
}
- private function expandServiceFields(Document $document): void
+ private function expandServices(Document $document): void
{
- if (!$document->isSet('services')) {
- return;
- }
-
$values = $document->getAttribute('services', []);
- $services = Config::getParam('services', []);
+ $services = [];
- foreach ($services as $service) {
+ foreach (Config::getParam('services', []) as $id => $service) {
if (!$service['optional']) {
continue;
}
- $key = $service['key'] ?? '';
- $value = $values[$key] ?? true;
- $document->setAttribute('serviceStatusFor' . ucfirst($key), $value);
- }
- }
- 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')) {
- return;
- }
-
- $authValues = $document->getAttribute('auths', []);
- $auth = Config::getParam('auth', []);
-
- $document->setAttribute('authLimit', $authValues['limit'] ?? 0);
- $document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG);
- $document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? 0);
- $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
- $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
- $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
- $document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false);
- $document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false);
- $document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false);
- $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []);
- $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false);
- $document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? false);
- $document->setAttribute('authMembershipsUserEmail', $authValues['membershipsUserEmail'] ?? false);
- $document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? false);
- $document->setAttribute('authMembershipsUserId', $authValues['membershipsUserId'] ?? false);
- $document->setAttribute('authMembershipsUserPhone', $authValues['membershipsUserPhone'] ?? false);
- $document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false);
-
- foreach ($auth as $method) {
- $key = $method['key'];
- $value = $authValues[$key] ?? true;
- $document->setAttribute('auth' . ucfirst($key), $value);
- }
- }
-
- private function expandOAuthProviders(Document $document): void
- {
- if (!$document->isSet('oAuthProviders')) {
- return;
- }
-
- $providers = Config::getParam('oAuthProviders', []);
- $providerValues = $document->getAttribute('oAuthProviders', []);
- $projectProviders = [];
-
- foreach ($providers as $key => $provider) {
- if (!$provider['enabled']) {
- // Disabled by Appwrite configuration, exclude from response
- continue;
- }
-
- $projectProviders[] = new Document([
- 'key' => $key,
- 'name' => $provider['name'] ?? '',
- 'appId' => $providerValues[$key . 'Appid'] ?? '',
- 'secret' => '', // Write-only: never expose the stored value
- 'enabled' => $providerValues[$key . 'Enabled'] ?? false,
+ $services[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$service['key']] ?? true,
]);
}
- $document->setAttribute('oAuthProviders', $projectProviders);
+ $document->setAttribute('services', $services);
+ }
+
+ private function expandProtocols(Document $document): void
+ {
+ $values = $document->getAttribute('apis', []);
+ $protocols = [];
+
+ foreach (Config::getParam('protocols', []) as $id => $api) {
+ $protocols[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$api['key']] ?? true,
+ ]);
+ }
+
+ $document->setAttribute('protocols', $protocols);
+ }
+
+ private function expandAuthMethods(Document $document): void
+ {
+ $values = $document->getAttribute('auths', []);
+ $authMethods = [];
+
+ foreach (Config::getParam('auth', []) as $id => $method) {
+ $authMethods[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$method['key']] ?? true
+ ]);
+ }
+
+ $document->setAttribute('authMethods', $authMethods);
}
}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php
new file mode 100644
index 0000000000..cb4a7dc93f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Auth method ID.',
+ 'default' => '',
+ 'example' => 'email-password',
+ 'enum' => \array_keys(Config::getParam('auth', [])),
+ 'enumSDKName' => 'ProjectAuthMethodId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Auth method status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectAuthMethod';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_AUTH_METHOD;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php
new file mode 100644
index 0000000000..918b1bc630
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Protocol ID.',
+ 'default' => '',
+ 'example' => 'graphql',
+ 'enum' => \array_keys(Config::getParam('protocols', [])),
+ 'enumSDKName' => 'ProjectProtocolId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Protocol status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectProtocol';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_PROTOCOL;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectService.php b/src/Appwrite/Utopia/Response/Model/ProjectService.php
new file mode 100644
index 0000000000..2e76dcfbe7
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectService.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Service ID.',
+ 'default' => '',
+ 'example' => 'sites',
+ 'enum' => \array_keys(\array_filter(Config::getParam('services', []), fn ($element) => $element['optional'])),
+ 'enumSDKName' => 'ProjectServiceId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Service status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectService';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_SERVICE;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Report.php b/src/Appwrite/Utopia/Response/Model/Report.php
new file mode 100644
index 0000000000..0c5baf9cdd
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Report.php
@@ -0,0 +1,99 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Report ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Report creation date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Report update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('appId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the third-party app that submitted the report.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Analyzer that produced this report. e.g. lighthouse, audit, databaseAnalyzer.',
+ 'default' => '',
+ 'example' => 'lighthouse',
+ ])
+ ->addRule('title', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Short, human-readable title for the report.',
+ 'default' => '',
+ 'example' => 'Lighthouse audit for https://appwrite.io/',
+ ])
+ ->addRule('summary', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Markdown summary describing the report.',
+ 'default' => '',
+ 'example' => 'Performance score 78. 4 opportunities found.',
+ ])
+ ->addRule('targetType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Plural noun describing what the report analyzes, e.g. databases, sites, urls.',
+ 'default' => '',
+ 'example' => 'urls',
+ ])
+ ->addRule('target', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Free-form target identifier (URL for lighthouse, resource ID for db).',
+ 'default' => '',
+ 'example' => 'https://appwrite.io/',
+ ])
+ ->addRule('categories', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Categories covered by the report, e.g. performance, accessibility.',
+ 'default' => [],
+ 'example' => ['performance', 'accessibility'],
+ 'array' => true,
+ ])
+ ->addRule('insights', [
+ 'type' => Response::MODEL_INSIGHT,
+ 'description' => 'Insights nested under this report.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true,
+ ])
+ ->addRule('analyzedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the report was analyzed in ISO 8601 format.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'Report';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_REPORT;
+ }
+}
diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php
index 99219ebf99..c34a6527f9 100644
--- a/tests/e2e/Scopes/ProjectCustom.php
+++ b/tests/e2e/Scopes/ProjectCustom.php
@@ -177,6 +177,10 @@ trait ProjectCustom
'project.policies.write',
'templates.read',
'templates.write',
+ 'insights.read',
+ 'insights.write',
+ 'reports.read',
+ 'reports.write',
],
]);
diff --git a/tests/e2e/Services/Advisor/AdvisorBase.php b/tests/e2e/Services/Advisor/AdvisorBase.php
new file mode 100644
index 0000000000..f228cf5591
--- /dev/null
+++ b/tests/e2e/Services/Advisor/AdvisorBase.php
@@ -0,0 +1,122 @@
+ 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ }
+
+ protected function getReport(string $reportId, ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId, $headers ?? $this->serverHeaders());
+ }
+
+ protected function listReports(array $params = [], ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports', $headers ?? $this->serverHeaders(), $params);
+ }
+
+ protected function getInsight(string $reportId, string $insightId, ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights/' . $insightId, $headers ?? $this->serverHeaders());
+ }
+
+ protected function listInsights(string $reportId, array $params = [], ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights', $headers ?? $this->serverHeaders(), $params);
+ }
+
+ public function testListReports(): void
+ {
+ $list = $this->listReports();
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertArrayHasKey('reports', $list['body']);
+ $this->assertArrayHasKey('total', $list['body']);
+ $this->assertIsArray($list['body']['reports']);
+ }
+
+ public function testGetReportMissing(): void
+ {
+ $missing = $this->getReport(ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testListInsightsMissingReport(): void
+ {
+ $missing = $this->listInsights(ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testGetInsightMissingReport(): void
+ {
+ $missing = $this->getInsight(ID::unique(), ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testReportsCreateAndUpdateNotExposed(): void
+ {
+ $create = $this->client->call(Client::METHOD_POST, '/reports', $this->serverHeaders(), [
+ 'reportId' => ID::unique(),
+ 'type' => 'audit',
+ 'title' => 'Read-only check',
+ 'targetType' => 'sites',
+ 'target' => 'home',
+ ]);
+ $this->assertSame(404, $create['headers']['status-code']);
+
+ $update = $this->client->call(Client::METHOD_PATCH, '/reports/' . ID::unique(), $this->serverHeaders(), [
+ 'title' => 'Read-only check',
+ ]);
+ $this->assertSame(404, $update['headers']['status-code']);
+ }
+
+ public function testDeleteReportMissing(): void
+ {
+ $delete = $this->client->call(Client::METHOD_DELETE, '/reports/' . ID::unique(), $this->serverHeaders());
+ $this->assertSame(404, $delete['headers']['status-code']);
+ $this->assertSame('report_not_found', $delete['body']['type']);
+ }
+
+ public function testInsightsCreateUpdateDeleteNotExposed(): void
+ {
+ $create = $this->client->call(
+ Client::METHOD_POST,
+ '/reports/' . ID::unique() . '/insights',
+ $this->serverHeaders(),
+ []
+ );
+ $this->assertSame(404, $create['headers']['status-code']);
+
+ $update = $this->client->call(
+ Client::METHOD_PATCH,
+ '/reports/' . ID::unique() . '/insights/' . ID::unique(),
+ $this->serverHeaders(),
+ ['status' => 'dismissed']
+ );
+ $this->assertSame(404, $update['headers']['status-code']);
+
+ $delete = $this->client->call(
+ Client::METHOD_DELETE,
+ '/reports/' . ID::unique() . '/insights/' . ID::unique(),
+ $this->serverHeaders()
+ );
+ $this->assertSame(404, $delete['headers']['status-code']);
+ }
+}
diff --git a/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php b/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php
new file mode 100644
index 0000000000..d91f95035e
--- /dev/null
+++ b/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php
@@ -0,0 +1,58 @@
+getProject()['$id'];
+
+ $userKey = $this->getNewKey([
+ // Advisor read APIs are protected by the underlying report/insight resource scopes.
+ 'insights.read',
+ 'reports.read',
+ ]);
+
+ $listed = $this->client->call(
+ Client::METHOD_GET,
+ '/reports',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $userKey,
+ ]
+ );
+
+ $this->assertSame(200, $listed['headers']['status-code']);
+
+ $create = $this->client->call(
+ Client::METHOD_POST,
+ '/reports',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $userKey,
+ ],
+ [
+ 'reportId' => ID::unique(),
+ 'type' => 'audit',
+ 'title' => 'Read-only check',
+ 'targetType' => 'sites',
+ 'target' => 'home',
+ ]
+ );
+
+ $this->assertSame(404, $create['headers']['status-code']);
+ }
+}
diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php
index 14442abb60..614349ae74 100644
--- a/tests/e2e/Services/Avatars/AvatarsBase.php
+++ b/tests/e2e/Services/Avatars/AvatarsBase.php
@@ -204,7 +204,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -216,7 +216,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 200,
'height' => 200,
]);
@@ -230,7 +230,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 300,
'height' => 300,
'quality' => 30,
@@ -258,7 +258,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 2001,
'height' => 300,
'quality' => 30,
diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php
index d83b450739..43dc58d44c 100644
--- a/tests/e2e/Services/Messaging/MessagingBase.php
+++ b/tests/e2e/Services/Messaging/MessagingBase.php
@@ -2525,6 +2525,119 @@ trait MessagingBase
$this->assertEquals(0, \count($message['body']['deliveryErrors']));
}
+ public function testCreatePushNotificationWithUsersRecipients(): void
+ {
+ if (empty(System::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) {
+ $this->markTestSkipped('Push DSN empty');
+ }
+
+ $dsn = new DSN(System::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'));
+ $to = $dsn->getParam('to');
+ $serviceAccountJSON = $dsn->getParam('serviceAccountJSON');
+
+ if (empty($to) || empty($serviceAccountJSON)) {
+ $this->markTestSkipped('Push provider not configured');
+ }
+
+ $provider1 = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM-Users-1',
+ 'serviceAccountJSON' => $serviceAccountJSON,
+ 'enabled' => true,
+ ]);
+
+ $this->assertEquals(201, $provider1['headers']['status-code']);
+
+ $provider2 = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM-Users-2',
+ 'serviceAccountJSON' => $serviceAccountJSON,
+ 'enabled' => true,
+ ]);
+
+ $this->assertEquals(201, $provider2['headers']['status-code']);
+
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => uniqid() . "@mail.org",
+ 'password' => 'password',
+ 'name' => 'Messaging User Recipients',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ $target1 = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'providerId' => $provider1['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target1['headers']['status-code']);
+
+ $target2 = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'providerId' => $provider2['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target2['headers']['status-code']);
+
+ $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'users' => [$user['body']['$id']],
+ 'title' => 'Test-Notification-Users',
+ 'body' => 'Test-Notification-Body-Users',
+ ]);
+
+ $this->assertEquals(201, $push['headers']['status-code']);
+
+ $pushMessageId = $push['body']['$id'];
+ $this->assertEventually(function () use ($pushMessageId) {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $pushMessageId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertContains($response['body']['status'], ['sent', 'failed']);
+ }, 30000, 500);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $pushMessageId, [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(2, $message['body']['deliveredTotal'] + \count($message['body']['deliveryErrors']));
+ }
+
public function testUpdatePushNotification(): void
{
$push = $this->setupSentPushData();
diff --git a/tests/e2e/Services/Project/AuthMethodsBase.php b/tests/e2e/Services/Project/AuthMethodsBase.php
index afa58a3640..cccdca9ea1 100644
--- a/tests/e2e/Services/Project/AuthMethodsBase.php
+++ b/tests/e2e/Services/Project/AuthMethodsBase.php
@@ -212,6 +212,7 @@ trait AuthMethodsBase
$headers = \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
$projectId = $this->getProject()['$id'];
@@ -322,7 +323,9 @@ trait AuthMethodsBase
];
if ($authenticated) {
- $headers = \array_merge($headers, $this->getHeaders());
+ $headers = \array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(
diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
index f86557a432..7b0479ea09 100644
--- a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
+++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
@@ -28,6 +28,7 @@ class OAuthGitHubIntegrationTest extends Scope
'content-type' => 'application/json',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Create new organization (team)
diff --git a/tests/e2e/Services/Project/PoliciesBase.php b/tests/e2e/Services/Project/PoliciesBase.php
index 04906c6c2b..43c09b55c3 100644
--- a/tests/e2e/Services/Project/PoliciesBase.php
+++ b/tests/e2e/Services/Project/PoliciesBase.php
@@ -1082,7 +1082,9 @@ trait PoliciesBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $headers;
@@ -1094,6 +1096,7 @@ trait PoliciesBase
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
+ 'x-appwrite-response-format' => '1.9.4',
]);
}
diff --git a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
index 378cd1800d..95d96c49e2 100644
--- a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesMembershipPrivacyIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Configure privacy to false
diff --git a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
index 2d0e15a70f..0da64eb50b 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesPasswordDictionaryIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// "password" is the top entry in the common-passwords dictionary and is 8 chars (min length).
diff --git a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
index c2dfd7be5e..1027d88222 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesPasswordHistoryIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Enable password history policy with limit 3
diff --git a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
index 3284fed16f..ebb94a2631 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
@@ -21,6 +21,7 @@ class PoliciesPasswordPersonalDataIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$setPersonalData = function (bool $enabled) use ($serverHeaders): void {
diff --git a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
index 1500a1dcfa..3e847b9ddb 100644
--- a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
@@ -23,6 +23,7 @@ class PoliciesSessionAlertIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
index 71562f52a5..582da65373 100644
--- a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesSessionDurationIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
index c9de2be9a5..b6f972ca3e 100644
--- a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesSessionInvalidationIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
index 5ddcd8aaa1..6025d80536 100644
--- a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesUserLimitIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$signupHeaders = [
diff --git a/tests/e2e/Services/Project/ProjectConsoleClientTest.php b/tests/e2e/Services/Project/ProjectConsoleClientTest.php
index 0ba69c21b6..a4c8b73efc 100644
--- a/tests/e2e/Services/Project/ProjectConsoleClientTest.php
+++ b/tests/e2e/Services/Project/ProjectConsoleClientTest.php
@@ -51,6 +51,88 @@ class ProjectConsoleClientTest extends Scope
$this->assertSame(404, $getProject['headers']['status-code']);
}
+ public function testGetProject(): void
+ {
+ $team = $this->createTeam('Get Project Team');
+ $project = $this->createProject($team['body']['$id'], 'Get Project');
+
+ $response = $this->client->call(Client::METHOD_GET, '/project', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $project['body']['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame($project['body']['$id'], $response['body']['$id']);
+ $this->assertNotEmpty($response['body']['$createdAt']);
+ $this->assertNotEmpty($response['body']['$updatedAt']);
+ $this->assertSame('Get Project', $response['body']['name']);
+ $this->assertSame($team['body']['$id'], $response['body']['teamId']);
+ $this->assertSame('active', $response['body']['status']);
+
+ // Auth methods
+ $this->assertIsArray($response['body']['authMethods']);
+ $this->assertNotEmpty($response['body']['authMethods']);
+ foreach ($response['body']['authMethods'] as $authMethod) {
+ $this->assertArrayHasKey('$id', $authMethod);
+ $this->assertArrayHasKey('enabled', $authMethod);
+ $this->assertIsBool($authMethod['enabled']);
+ }
+
+ // Services
+ $this->assertIsArray($response['body']['services']);
+ $this->assertNotEmpty($response['body']['services']);
+ foreach ($response['body']['services'] as $service) {
+ $this->assertArrayHasKey('$id', $service);
+ $this->assertArrayHasKey('enabled', $service);
+ $this->assertIsBool($service['enabled']);
+ }
+
+ // Protocols
+ $this->assertIsArray($response['body']['protocols']);
+ $this->assertNotEmpty($response['body']['protocols']);
+ foreach ($response['body']['protocols'] as $protocol) {
+ $this->assertArrayHasKey('$id', $protocol);
+ $this->assertArrayHasKey('enabled', $protocol);
+ $this->assertIsBool($protocol['enabled']);
+ }
+
+ // SMTP defaults
+ $this->assertFalse($response['body']['smtpEnabled']);
+ $this->assertSame('', $response['body']['smtpSenderEmail']);
+ $this->assertSame('', $response['body']['smtpSenderName']);
+ $this->assertSame('', $response['body']['smtpReplyToEmail']);
+ $this->assertSame('', $response['body']['smtpReplyToName']);
+ $this->assertSame('', $response['body']['smtpHost']);
+ $this->assertSame('', $response['body']['smtpPort']);
+ $this->assertSame('', $response['body']['smtpUsername']);
+ $this->assertSame('', $response['body']['smtpPassword']);
+ $this->assertSame('', $response['body']['smtpSecure']);
+
+ // Other fields
+ $this->assertIsArray($response['body']['labels']);
+ $this->assertIsArray($response['body']['devKeys']);
+ $this->assertSame(0, $response['body']['pingCount']);
+ $this->assertSame('', $response['body']['pingedAt']);
+
+ // Ensure old flattened fields are not present
+ $this->assertArrayNotHasKey('description', $response['body']);
+ $this->assertArrayNotHasKey('logo', $response['body']);
+ $this->assertArrayNotHasKey('url', $response['body']);
+ $this->assertArrayNotHasKey('authDuration', $response['body']);
+ $this->assertArrayNotHasKey('authLimit', $response['body']);
+ $this->assertArrayNotHasKey('authSessionsLimit', $response['body']);
+ $this->assertArrayNotHasKey('authPasswordHistory', $response['body']);
+ $this->assertArrayNotHasKey('authPasswordDictionary', $response['body']);
+ $this->assertArrayNotHasKey('authPersonalDataCheck', $response['body']);
+ $this->assertArrayNotHasKey('authDisposableEmails', $response['body']);
+ $this->assertArrayNotHasKey('authCanonicalEmails', $response['body']);
+ $this->assertArrayNotHasKey('authFreeEmails', $response['body']);
+ $this->assertArrayNotHasKey('oAuthProviders', $response['body']);
+ $this->assertArrayNotHasKey('platforms', $response['body']);
+ $this->assertArrayNotHasKey('webhooks', $response['body']);
+ $this->assertArrayNotHasKey('keys', $response['body']);
+ }
+
protected function createTeam(string $name): array
{
$response = $this->client->call(Client::METHOD_POST, '/teams', $this->getConsoleSessionHeaders(), [
diff --git a/tests/e2e/Services/Project/ProtocolsBase.php b/tests/e2e/Services/Project/ProtocolsBase.php
index f828994ea3..092ed97b6a 100644
--- a/tests/e2e/Services/Project/ProtocolsBase.php
+++ b/tests/e2e/Services/Project/ProtocolsBase.php
@@ -248,6 +248,7 @@ trait ProtocolsBase
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
// Disable via the legacy `/status` alias
@@ -278,7 +279,9 @@ trait ProtocolsBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [
diff --git a/tests/e2e/Services/Project/ServicesBase.php b/tests/e2e/Services/Project/ServicesBase.php
index b5f94f8181..0c8b337315 100644
--- a/tests/e2e/Services/Project/ServicesBase.php
+++ b/tests/e2e/Services/Project/ServicesBase.php
@@ -246,6 +246,7 @@ trait ServicesBase
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
// Disable via the legacy `/status` alias
@@ -276,7 +277,9 @@ trait ServicesBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId, $headers, [
diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
index 8b0c1af57f..aa5e6911f1 100644
--- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
+++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
@@ -46,6 +46,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4'
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -68,6 +69,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -89,6 +91,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => '',
@@ -101,6 +104,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -127,6 +131,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'MultiDB Project',
@@ -210,6 +215,7 @@ class ProjectsConsoleClientTest extends Scope
$getProject = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $getProject['headers']['status-code']);
@@ -234,6 +240,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => $projectId,
'name' => 'Original Project',
@@ -249,6 +256,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => $projectId,
'name' => 'Project Duplicate',
@@ -298,6 +306,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Team 1 Project',
@@ -318,6 +327,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/team', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'teamId' => $team2,
]);
@@ -341,6 +351,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -353,6 +364,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders(), [
'search' => $id
]));
@@ -364,6 +376,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders(), [
'search' => 'Project Test'
]));
@@ -390,6 +403,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -408,6 +422,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::equal('teamId', [$team['body']['$id']])->toString(),
@@ -422,6 +437,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
@@ -435,6 +451,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::offset(1)->toString(),
@@ -447,6 +464,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Project Test 2'])->toString(),
@@ -461,6 +479,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::orderDesc()->toString(),
@@ -474,6 +493,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -483,6 +503,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(),
@@ -498,6 +519,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(),
@@ -524,6 +546,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Query Select Test Project',
@@ -540,6 +563,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
@@ -569,6 +593,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4'
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(),
@@ -600,6 +625,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId'])->toString(),
@@ -631,6 +657,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
@@ -661,6 +688,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'platforms'])->toString(),
@@ -690,6 +718,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(),
@@ -719,6 +748,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['*'])->toString(),
@@ -749,6 +779,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'invalidAttribute'])->toString(),
@@ -783,6 +814,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1027,6 +1059,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1041,17 +1074,17 @@ class ProjectsConsoleClientTest extends Scope
$this->assertNotEmpty($response['body']['$updatedAt']);
$this->assertNotFalse(\strtotime($response['body']['$updatedAt']));
- $this->assertEquals('My description', $response['body']['description']);
+ // $this->assertEquals('My description', $response['body']['description']); // No longer supported
$this->assertEquals($team['body']['$id'], $response['body']['teamId']);
$this->assertEquals('active', $response['body']['status']);
- $this->assertEquals('https://google.com/logo.png', $response['body']['logo']);
- $this->assertEquals('https://myapp.com/', $response['body']['url']);
- $this->assertEquals('Legal company', $response['body']['legalName']);
- $this->assertEquals('Slovakia', $response['body']['legalCountry']);
- $this->assertEquals('Custom state', $response['body']['legalState']);
- $this->assertEquals('Košice', $response['body']['legalCity']);
- $this->assertEquals('Main street 32', $response['body']['legalAddress']);
- $this->assertEquals('TAXID_123456', $response['body']['legalTaxId']);
+ // $this->assertEquals('https://google.com/logo.png', $response['body']['logo']); // No longer supported
+ // $this->assertEquals('https://myapp.com/', $response['body']['url']); // No longer supported
+ // $this->assertEquals('Legal company', $response['body']['legalName']); // No longer supported
+ // $this->assertEquals('Slovakia', $response['body']['legalCountry']); // No longer supported
+ // $this->assertEquals('Custom state', $response['body']['legalState']); // No longer supported
+ // $this->assertEquals('Košice', $response['body']['legalCity']); // No longer supported
+ // $this->assertEquals('Main street 32', $response['body']['legalAddress']); // No longer supported
+ // $this->assertEquals('TAXID_123456', $response['body']['legalTaxId']); // No longer supported
$this->assertEquals(135, $response['body']['authDuration']);
$this->assertEquals(54, $response['body']['authLimit']);
$this->assertEquals(7, $response['body']['authSessionsLimit']);
@@ -1411,6 +1444,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1482,6 +1516,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/empty', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
@@ -1490,9 +1525,10 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
- $this->assertEquals(400, $response['headers']['status-code']);
+ $this->assertEquals(404, $response['headers']['status-code']);
}
public function testGetProjectUsage(): void
@@ -1520,6 +1556,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1536,6 +1573,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -1556,6 +1594,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => '',
@@ -1580,6 +1619,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'mailer@appwrite.io',
@@ -1605,6 +1645,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1624,6 +1665,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'fail@appwrite.io',
@@ -1663,6 +1705,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1851,6 +1894,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Session Alert Locale Fallback Test',
@@ -1864,6 +1908,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'mailer@appwrite.io',
@@ -1932,6 +1977,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['authSessionAlerts']);
@@ -2054,6 +2100,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2166,6 +2213,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2189,6 +2237,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Session Invalidation Test Project',
@@ -2203,6 +2252,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2220,6 +2270,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2237,6 +2288,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2261,6 +2313,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -2280,6 +2333,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => $key,
'appId' => 'AppId-' . ucfirst($key),
@@ -2293,6 +2347,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2320,6 +2375,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => $key,
'enabled' => $i === 0 ? false : true // On first provider, test enabled=false
@@ -2334,6 +2390,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2363,6 +2420,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => 'unknown',
'appId' => 'AppId',
@@ -2390,6 +2448,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -2445,6 +2504,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2855,6 +2915,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), []);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -2864,6 +2925,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
'phone' => '+1655513432',
@@ -2877,6 +2939,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2892,6 +2955,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2907,6 +2971,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2922,6 +2987,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2937,6 +3003,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2952,6 +3019,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2979,6 +3047,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => $numbers
]);
@@ -2992,6 +3061,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => []
]);
@@ -3000,6 +3070,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -3323,6 +3394,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -3358,6 +3430,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3387,6 +3460,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3412,6 +3486,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -3442,6 +3517,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3464,6 +3540,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3487,6 +3564,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -3527,6 +3605,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3603,6 +3682,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3679,6 +3759,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -4437,6 +4518,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -5485,6 +5567,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
@@ -5510,6 +5593,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $project['headers']['status-code']);
@@ -5533,6 +5617,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $project['headers']['status-code']);
@@ -5556,6 +5641,7 @@ class ProjectsConsoleClientTest extends Scope
$project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 1',
@@ -5566,6 +5652,7 @@ class ProjectsConsoleClientTest extends Scope
$project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 2',
@@ -6908,6 +6995,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 1',
@@ -6952,6 +7040,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip'])->toString(),
@@ -6964,6 +7053,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -6975,6 +7065,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
@@ -6987,6 +7078,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip', 'imagine'])->toString(),
@@ -7000,6 +7092,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 2',
@@ -7028,6 +7121,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
@@ -7042,6 +7136,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -7055,6 +7150,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -7069,6 +7165,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip', 'imagine'])->toString(),
@@ -7174,6 +7271,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
]);
@@ -7270,6 +7368,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'name' => $newProjectName,
@@ -7286,6 +7385,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'name' => $newProjectName,
diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php
index 32ee13b5f7..4811fc3737 100644
--- a/tests/e2e/Services/Proxy/ProxyBase.php
+++ b/tests/e2e/Services/Proxy/ProxyBase.php
@@ -171,8 +171,8 @@ trait ProxyBase
$siteId = $this->setupSite()['siteId'];
- $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
- $this->assertNotEmpty($ruleId);
+ $ruleId301 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
+ $this->assertNotEmpty($ruleId301);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(200, $response['headers']['status-code']);
@@ -187,8 +187,8 @@ trait ProxyBase
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$domain = \uniqid() . '-redirect-307.custom.localhost';
- $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
- $this->assertNotEmpty($ruleId);
+ $ruleId307 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
+ $this->assertNotEmpty($ruleId307);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
@@ -209,8 +209,9 @@ trait ProxyBase
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
+ $this->cleanupRule($ruleId301);
+ $this->cleanupRule($ruleId307);
$this->cleanupSite($siteId);
- $this->cleanupRule($ruleId);
}
public function testCreateFunctionRule(): void
diff --git a/tests/unit/Advisor/Validator/CTAsTest.php b/tests/unit/Advisor/Validator/CTAsTest.php
new file mode 100644
index 0000000000..5511910072
--- /dev/null
+++ b/tests/unit/Advisor/Validator/CTAsTest.php
@@ -0,0 +1,241 @@
+assertFalse($validator->isValid('not-an-array'));
+ $this->assertFalse($validator->isValid(42));
+ $this->assertFalse($validator->isValid(null));
+ }
+
+ public function testAcceptsEmptyArray(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([]));
+ }
+
+ public function testAcceptsCompleteEntry(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => [
+ 'databaseId' => 'main',
+ 'tableId' => 'orders',
+ ],
+ ]]));
+ }
+
+ public function testAcceptsEntryWithoutParams(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryMissingRequiredKeys(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([['label' => 'x']]));
+ $this->assertFalse($validator->isValid([['label' => 'x', 'service' => 'tablesDB']]));
+ $this->assertFalse($validator->isValid([['label' => 'x', 'method' => 'createIndex']]));
+ }
+
+ public function testRejectsEntryWithEmptyStrings(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => '',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithNonStringFields(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 123,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithScalarParams(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => 'not-a-map',
+ ]]));
+ }
+
+ public function testReportsArrayType(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isArray());
+ $this->assertSame($validator::TYPE_ARRAY, $validator->getType());
+ }
+
+ public function testRejectsMoreThanMaxCount(): void
+ {
+ $validator = new CTAs(maxCount: 3);
+
+ $entries = [];
+ for ($i = 0; $i < 4; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertFalse($validator->isValid($entries));
+ $this->assertStringContainsString('maximum of 3', $validator->getDescription());
+ }
+
+ public function testAcceptsExactlyMaxCount(): void
+ {
+ $validator = new CTAs(maxCount: 3);
+
+ $entries = [];
+ for ($i = 0; $i < 3; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertTrue($validator->isValid($entries));
+ }
+
+ public function testAcceptsObjectParams(): void
+ {
+ $validator = new CTAs();
+
+ $entry = [
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => new \stdClass(),
+ ];
+
+ $this->assertTrue($validator->isValid([$entry]));
+ }
+
+ public function testRejectsEntryWithEmptyService(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => '',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithEmptyMethod(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => '',
+ ]]));
+ }
+
+ public function testRejectsUnknownService(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'nonExistentService',
+ 'method' => 'createIndex',
+ ]]));
+ $this->assertStringContainsString('service', $validator->getDescription());
+ }
+
+ public function testRejectsUnknownMethod(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'nonExistentMethod',
+ ]]));
+ $this->assertStringContainsString('method', $validator->getDescription());
+ }
+
+ public function testAcceptsCustomAllowedLists(): void
+ {
+ $validator = new CTAs(
+ allowedServices: ['custom'],
+ allowedMethods: ['doThing'],
+ );
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Custom action',
+ 'service' => 'custom',
+ 'method' => 'doThing',
+ ]]));
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Custom action',
+ 'service' => 'tablesDB',
+ 'method' => 'doThing',
+ ]]));
+ }
+
+ public function testDefaultMaxCountIsSixteen(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertSame(CTAs::MAX_COUNT_DEFAULT, 16);
+
+ $entries = [];
+ for ($i = 0; $i < 16; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertTrue($validator->isValid($entries));
+
+ $entries[] = [
+ 'label' => 'Label 16',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+
+ $this->assertFalse($validator->isValid($entries));
+ }
+}
diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php
index f5a30a5500..74c68303f4 100644
--- a/tests/unit/Utopia/ResponseTest.php
+++ b/tests/unit/Utopia/ResponseTest.php
@@ -34,10 +34,11 @@ class ResponseTest extends TestCase
$this->assertTrue($this->response->hasFilters());
$this->assertCount(2, $this->response->getFilters());
- $output = $this->response->applyFilters([
+ $content = [
'initial' => true,
'first' => false
- ], 'test');
+ ];
+ $output = $this->response->applyFilters($content, 'test', raw: new Document($content));
$this->assertArrayHasKey('initial', $output);
$this->assertTrue($output['initial']);