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']);