diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2ddc416f3..ac16c80264 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -427,6 +427,7 @@ jobs: FunctionsSchedule, GraphQL, Health, + Advisor, Locale, Projects, Realtime, 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..9ad223a3ff 100644 --- a/app/cli.php +++ b/app/cli.php @@ -2,10 +2,11 @@ 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\Database as DatabasePublisher; +use Appwrite\Event\Publisher\Delete as DeletePublisher; +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,12 +282,18 @@ $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('queueForDeletes', function (Publisher $publisher) { - return new Delete($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('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher( + $publisherDatabases, + new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME)) +), ['publisherDatabases']); +$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher( + $publisher, + new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME)) +), ['publisher']); $container->set('logError', function (Registry $register) { return function (Throwable $error, string $namespace, string $action) use ($register) { Console::error('[Error] Timestamp: ' . date('c', time())); 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/organization.php b/app/config/scopes/organization.php index 228a1437f2..d74452f259 100644 --- a/app/config/scopes/organization.php +++ b/app/config/scopes/organization.php @@ -4,17 +4,23 @@ return [ "projects.read" => [ - "description" => 'Access to read organization\'s projects', + "description" => 'Access to read organization projects', + "category" => "Projects", ], "projects.write" => [ "description" => - "Access to create, update, and delete projects in organization", + "Access to create, update, and delete organization projects", + "category" => "Projects", ], "devKeys.read" => [ "description" => 'Access to read project\'s development keys', + "category" => "Other", + "deprecated" => true, ], "devKeys.write" => [ "description" => "Access to create, update, and delete project\'s development keys", + "category" => "Other", + "deprecated" => true, ], ]; 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/account.php b/app/controllers/api/account.php index 3f68180fc1..5b08c2f3cc 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -11,10 +11,11 @@ use Appwrite\Auth\Validator\PersonalData; use Appwrite\Auth\Validator\Phone; use Appwrite\Bus\Events\SessionCreated; use Appwrite\Detector\Detector; -use Appwrite\Event\Delete; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; use Appwrite\Event\Message\Mail as MailMessage; use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; @@ -499,9 +500,9 @@ Http::delete('/v1/account') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('authorization') - ->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) { + ->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes, Authorization $authorization) { if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -525,9 +526,11 @@ Http::delete('/v1/account') $dbForProject->deleteDocument('users', $user->getId()); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($user); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $user, + )); $queueForEvents ->setParam('userId', $user->getId()) @@ -610,12 +613,12 @@ Http::delete('/v1/account/sessions') ->inject('dbForProject') ->inject('locale') ->inject('queueForEvents') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('store') ->inject('proofForToken') ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -646,10 +649,11 @@ Http::delete('/v1/account/sessions') $queueForEvents ->setPayload($response->output($session, Response::MODEL_SESSION)); - $queueForDeletes - ->setType(DELETE_TYPE_SESSION_TARGETS) - ->setDocument($session) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_SESSION_TARGETS, + document: $session, + )); } } @@ -744,12 +748,12 @@ Http::delete('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('locale') ->inject('queueForEvents') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('store') ->inject('proofForToken') ->inject('domainVerification') ->inject('cookieDomain') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -791,10 +795,11 @@ Http::delete('/v1/account/sessions/:sessionId') ->setParam('sessionId', $session->getId()) ->setPayload($response->output($session, Response::MODEL_SESSION)); - $queueForDeletes - ->setType(DELETE_TYPE_SESSION_TARGETS) - ->setDocument($session) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_SESSION_TARGETS, + document: $session, + )); $response->noContent(); return; @@ -4743,13 +4748,13 @@ Http::delete('/v1/account/targets/:targetId/push') )) ->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject']) ->inject('queueForEvents') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('user') ->inject('request') ->inject('response') ->inject('dbForProject') ->inject('authorization') - ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { + ->action(function (string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) { $target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId)); if ($target->isEmpty()) { @@ -4764,9 +4769,11 @@ Http::delete('/v1/account/targets/:targetId/push') $dbForProject->purgeCachedDocument('users', $user->getId()); - $queueForDeletes - ->setType(DELETE_TYPE_TARGET) - ->setDocument($target); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_TARGET, + document: $target, + )); $queueForEvents ->setParam('userId', $user->getId()) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 5d41b50ca5..0b63ff6928 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3,9 +3,10 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; -use Appwrite\Event\Delete; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; @@ -2712,9 +2713,9 @@ Http::delete('/v1/messaging/topics/:topicId') ->param('topicId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Topic ID.', false, ['dbForProject']) ->inject('queueForEvents') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('response') - ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) { + ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, DeletePublisher $publisherForDeletes, Response $response) { $topic = $dbForProject->getDocument('topics', $topicId); if ($topic->isEmpty()) { @@ -2723,9 +2724,11 @@ Http::delete('/v1/messaging/topics/:topicId') $dbForProject->deleteDocument('topics', $topicId); - $queueForDeletes - ->setType(DELETE_TYPE_TOPIC) - ->setDocument($topic); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_TOPIC, + document: $topic, + )); $queueForEvents ->setParam('topicId', $topic->getId()); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 796a40d5b8..aaa5b7e851 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -11,8 +11,9 @@ use Appwrite\Auth\Validator\Phone; use Appwrite\Deletes\Identities as DeleteIdentities; use Appwrite\Deletes\Targets as DeleteTargets; use Appwrite\Detector\Detector; -use Appwrite\Event\Delete; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Appwrite\Locale\GeoRecord; @@ -2585,8 +2586,8 @@ Http::delete('/v1/users/:userId') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForDeletes') - ->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) { + ->inject('publisherForDeletes') + ->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes) { $user = $dbForProject->getDocument('users', $userId); @@ -2601,9 +2602,11 @@ Http::delete('/v1/users/:userId') DeleteIdentities::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()])); DeleteTargets::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()])); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($clone); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $clone, + )); $queueForEvents ->setParam('userId', $user->getId()) @@ -2636,10 +2639,10 @@ Http::delete('/v1/users/:userId/targets/:targetId') ->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject']) ->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject']) ->inject('queueForEvents') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('response') ->inject('dbForProject') - ->action(function (string $userId, string $targetId, Event $queueForEvents, Delete $queueForDeletes, Response $response, Database $dbForProject) { + ->action(function (string $userId, string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Response $response, Database $dbForProject) { $user = $dbForProject->getDocument('users', $userId); if ($user->isEmpty()) { @@ -2659,9 +2662,11 @@ Http::delete('/v1/users/:userId/targets/:targetId') $dbForProject->deleteDocument('targets', $target->getId()); $dbForProject->purgeCachedDocument('users', $user->getId()); - $queueForDeletes - ->setType(DELETE_TYPE_TARGET) - ->setDocument($target); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_TARGET, + document: $target, + )); $queueForEvents ->setParam('userId', $user->getId()) diff --git a/app/controllers/general.php b/app/controllers/general.php index d5187e0f1b..4fa1308ea8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -7,9 +7,10 @@ use Ahc\Jwt\JWTException; use Appwrite\Auth\Key; use Appwrite\Bus\Events\ExecutionCompleted; use Appwrite\Bus\Events\RequestCompleted; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; use Appwrite\Event\Publisher\Certificate; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Locale\GeoRecord; use Appwrite\Network\Cors; @@ -74,7 +75,7 @@ use Utopia\Validator\Text; Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount) +function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { $host = $request->getHostname(); if (!empty($previewHostname)) { @@ -784,12 +785,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES; - $queueForDeletes - ->setProject($project) - ->setResourceType($resourceType) - ->setResource($resource->getSequence()) - ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_EXECUTIONS_LIMIT, + resource: (string) $resource->getSequence(), + resourceType: $resourceType, + )); } return true; @@ -850,9 +851,9 @@ Http::init() ->inject('apiKey') ->inject('cors') ->inject('authorization') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, GeoRecord $geoRecord, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, GeoRecord $geoRecord, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { /* * Appwrite Router */ @@ -860,7 +861,7 @@ Http::init() $platformHostnames = $platform['hostnames'] ?? []; // Only run Router when external domain if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1161,16 +1162,16 @@ Http::options() ->inject('apiKey') ->inject('cors') ->inject('authorization') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { /* * Appwrite Router */ $platformHostnames = $platform['hostnames'] ?? []; // Only run Router when external domain if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1563,15 +1564,15 @@ Http::get('/robots.txt') ->inject('previewHostname') ->inject('apiKey') ->inject('authorization') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } @@ -1597,15 +1598,15 @@ Http::get('/humans.txt') ->inject('previewHostname') ->inject('apiKey') ->inject('authorization') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) { + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, GeoRecord $geoRecord, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { $platformHostnames = $platform['hostnames'] ?? []; if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geoRecord, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { $utopia->getRoute()?->label('router', true); } } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 8365274e98..6e5167660a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -4,13 +4,12 @@ use Appwrite\Auth\Key; use Appwrite\Auth\MFA\Type\TOTP; use Appwrite\Bus\Events\RequestCompleted; 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; @@ -476,6 +475,85 @@ Http::init() } }); +Http::init() + ->groups(['api']) + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('project') + ->inject('user') + ->inject('timelimit') + ->inject('devKey') + ->inject('authorization') + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { + $response->setUser($user); + $request->setUser($user); + + $roles = $authorization->getRoles(); + $shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' + && ! $user->isApp($roles) + && ! $user->isPrivileged($roles) + && $devKey->isEmpty(); + + $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + $closestLimit = null; + + foreach ($abuseKeyLabel as $abuseKey) { + $isRateLimited = false; + + try { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600)); + $timeLimit + ->setParam('{projectId}', $project->getId()) + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname() . $route->getPath()) + ->setParam('{method}', $request->getMethod()) + ->setParam('{chunkId}', (int) ($start / ($end + 1 - $start))); + + foreach ($request->getParams() as $key => $value) { + if (! empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + $remaining = $timeLimit->remaining(); + $limit = $timeLimit->limit(); + $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); + + if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { + $closestLimit = $remaining; + $response + ->addHeader('X-RateLimit-Limit', $limit) + ->addHeader('X-RateLimit-Remaining', $remaining) + ->addHeader('X-RateLimit-Reset', $time); + } + + if ($shouldCheckAbuse) { + $isRateLimited = $abuse->check(); + } + } catch (\Throwable $th) { + \error_log((string) $th); + + continue; + } + + if ($isRateLimited) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); + } + } + }); + Http::init() ->groups(['api']) ->inject('utopia') @@ -485,22 +563,18 @@ Http::init() ->inject('user') ->inject('queueForEvents') ->inject('auditContext') - ->inject('queueForDeletes') - ->inject('queueForDatabase') ->inject('usage') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('dbForProject') - ->inject('timelimit') ->inject('resourceToken') ->inject('mode') ->inject('apiKey') ->inject('plan') - ->inject('devKey') ->inject('telemetry') ->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, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, 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); @@ -517,70 +591,6 @@ Http::init() default => '', }; - /* - * Abuse Check - */ - - $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); - $timeLimitArray = []; - - $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; - - foreach ($abuseKeyLabel as $abuseKey) { - $start = $request->getContentRangeStart(); - $end = $request->getContentRangeEnd(); - $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600)); - $timeLimit - ->setParam('{projectId}', $project->getId()) - ->setParam('{userId}', $user->getId()) - ->setParam('{userAgent}', $request->getUserAgent('')) - ->setParam('{ip}', $request->getIP()) - ->setParam('{url}', $request->getHostname() . $route->getPath()) - ->setParam('{method}', $request->getMethod()) - ->setParam('{chunkId}', (int) ($start / ($end + 1 - $start))); - $timeLimitArray[] = $timeLimit; - } - - $closestLimit = null; - - $roles = $authorization->getRoles(); - $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); - - foreach ($timeLimitArray as $timeLimit) { - foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - if (! empty($value)) { - $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); - } - } - - $abuse = new Abuse($timeLimit); - $remaining = $timeLimit->remaining(); - - $limit = $timeLimit->limit(); - $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); - - if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { - $closestLimit = $remaining; - $response - ->addHeader('X-RateLimit-Limit', $limit) - ->addHeader('X-RateLimit-Remaining', $remaining) - ->addHeader('X-RateLimit-Reset', $time); - } - - $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; - - if ( - $enabled // Abuse is enabled - && ! $isAppUser // User is not API key - && ! $isPrivilegedUser // User is not an admin - && $devKey->isEmpty() // request doesn't not contain development key - && $abuse->check() // Route is rate-limited - ) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); - } - } - /** * TODO: (@loks0n) * Avoid mutating the message across file boundaries - it's difficult to reason about at scale. @@ -610,20 +620,14 @@ Http::init() $auditContext->user = $userClone; } - /* 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'); if ($useCache) { $route = $utopia->match($request); + $roles = $authorization->getRoles(); + $isAppUser = $user->isApp($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; - $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles()); + $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles); $key = $request->cacheIdentifier(); Span::add('storage.cache.key', $key); @@ -644,7 +648,7 @@ Http::init() $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); - $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); + $isPrivilegedUser = $user->isPrivileged($roles); if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -806,9 +810,7 @@ Http::shutdown() ->inject('publisherForAudits') ->inject('usage') ->inject('publisherForUsage') - ->inject('queueForDeletes') - ->inject('queueForDatabase') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') @@ -818,7 +820,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, 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(); @@ -847,9 +849,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; } } @@ -959,14 +967,6 @@ Http::shutdown() $publisherForAudits->enqueue(AuditMessage::fromContext($auditContext)); } - if (! empty($queueForDeletes->getType())) { - $queueForDeletes->trigger(); - } - - if (! empty($queueForDatabase->getType())) { - $queueForDatabase->trigger(); - } - // Cache label $useCache = $route->getLabel('cache', false); if ($useCache) { diff --git a/app/init/constants.php b/app/init/constants.php index 17afc35ae9..bdc8e67fae 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'; @@ -458,3 +514,7 @@ const CSV_ALLOWED_DATABASE_TYPES = [ DATABASE_TYPE_TABLESDB, DATABASE_TYPE_VECTORSDB ]; + +const VCS_DEPLOYMENT_SKIP_PATTERNS = [ + '[skip ci]', +]; 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 f983c43f0a..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; @@ -182,6 +184,7 @@ 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; @@ -291,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()); @@ -515,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/registers.php b/app/init/registers.php index e9ed5e7733..42117b7253 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -239,6 +239,12 @@ $register->set('pools', function () { 'multiple' => true, 'schemes' => ['redis'], ], + 'lock' => [ + 'type' => 'lock', + 'dsns' => $fallbackForRedis, + 'multiple' => false, + 'schemes' => ['redis'], + ], ]; $maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151); @@ -368,6 +374,8 @@ $register->set('pools', function () { } return $adapter; + case 'lock': + return $resource(); default: throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation."); } diff --git a/app/init/resources.php b/app/init/resources.php index ca30660797..f2964fad66 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -4,7 +4,10 @@ use Appwrite\Event\Event; 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\Database as DatabasePublisher; +use Appwrite\Event\Publisher\Delete as DeletePublisher; 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; @@ -26,6 +29,7 @@ use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\DI\Container; use Utopia\DSN\DSN; +use Utopia\Lock\Distributed; use Utopia\Pools\Group; use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Publisher; @@ -108,6 +112,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)) @@ -120,6 +128,14 @@ $container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPubl $publisher, new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME)) ), ['publisher']); +$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher( + $publisherDatabases, + new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME)) +), ['publisherDatabases']); +$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher( + $publisher, + new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME)) +), ['publisher']); $container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher( $publisher, new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME)) @@ -233,6 +249,16 @@ $container->set('redis', function () { return $redis; }); +$container->set('locks', function (Group $pools) { + return function (string $key, int $ttl, callable $callback, float $timeout = 0.0) use ($pools): mixed { + return $pools->get('lock')->use(function (\Redis $redis) use ($key, $ttl, $callback, $timeout) { + $lock = new Distributed($redis, $key, ttl: $ttl); + + return $lock->withLock($callback, timeout: $timeout); + }); + }; +}, ['pools']); + $container->set('timelimit', function (\Redis $redis) { return function (string $key, int $limit, int $time) use ($redis) { return new TimeLimitRedis($key, $limit, $time, $redis); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 404f6cbcdc..417dcc7e4f 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -5,10 +5,9 @@ use Ahc\Jwt\JWTException; use Appwrite\Auth\Key; use Appwrite\Databases\TransactionState; 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; @@ -51,6 +50,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; @@ -109,14 +109,15 @@ return function (Container $context): void { }); // Per-request queue resources (stateful, accumulate event data during request) - $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('queueForFunctions', fn (Publisher $publisher) => new Func($publisher), ['publisher']); + $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')); @@ -638,7 +639,7 @@ return function (Container $context): void { return; }, ['user', 'store', 'proofForToken']); - $context->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; } @@ -694,7 +695,7 @@ return function (Container $context): 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; @@ -706,9 +707,15 @@ return function (Container $context): 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'))) { @@ -888,7 +895,6 @@ return function (Container $context): 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(); @@ -903,7 +909,7 @@ return function (Container $context): void { $document, $response, $queueForEventsClone->from($queueForEvents), - $queueForFunctions->from($queueForEvents), + $publisherForFunctions, $queueForWebhooks->from($queueForEvents), $queueForRealtime->from($queueForEvents) )) @@ -912,7 +918,7 @@ return function (Container $context): 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']); $context->set('schema', function ($utopia, $dbForProject, $authorization) { @@ -1251,7 +1257,7 @@ return function (Container $context): void { $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 { 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..3585421a28 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -1,9 +1,7 @@ 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']); @@ -343,10 +334,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 1696a57d05..400e3c1822 100644 --- a/composer.json +++ b/composer.json @@ -54,28 +54,29 @@ "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": "^2.0@RC", "utopia-php/fetch": "^1.1", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", + "utopia-php/lock": "0.2.*", "utopia-php/logger": "0.8.*", "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.*", - "utopia-php/platform": "^1.0@RC", + "utopia-php/platform": "1.0.0-rc2", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", "utopia-php/preloader": "0.2.*", @@ -85,7 +86,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 52a4a955b2..66d8f62925 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": "9db08148f3a8f53bd972eb7b3a835b3b", + "content-hash": "035685d1335039f13e16d0532c874b21", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "23.1.0", + "version": "23.1.1", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa" + "reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa", - "reference": "2f275921f10ceb7cff99f2d463f7328b296234fa", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/fd7c0f0bf5ddf334533534b20ed967cfb400f6ea", + "reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea", "shasum": "" }, "require": { @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/23.1.1", "url": "https://appwrite.io/support" }, - "time": "2026-05-08T13:44:58+00:00" + "time": "2026-05-12T11:03:36+00:00" }, { "name": "appwrite/php-clamav", @@ -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", @@ -4433,6 +4498,57 @@ }, "time": "2025-08-12T12:58:26+00:00" }, + { + "name": "utopia-php/lock", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/lock.git", + "reference": "49317c9493d8f747e4299aa24c22862aa5f6e106" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/lock/zipball/49317c9493d8f747e4299aa24c22862aa5f6e106", + "reference": "49317c9493d8f747e4299aa24c22862aa5f6e106", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "laravel/pint": "1.*", + "phpstan/phpstan": "2.*", + "phpunit/phpunit": "11.*", + "swoole/ide-helper": "*" + }, + "suggest": { + "ext-pcntl": "Required to run the File lock tests", + "ext-redis": "Required for the Distributed lock", + "ext-swoole": "Required for the Mutex and Semaphore locks (>=6.0)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Lock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Appwrite Team", + "email": "team@appwrite.io" + } + ], + "description": "Mutex, semaphore, file and distributed locks for PHP — one interface, four backends.", + "support": { + "issues": "https://github.com/utopia-php/lock/issues", + "source": "https://github.com/utopia-php/lock/tree/0.2.0" + }, + "time": "2026-04-24T10:47:56+00:00" + }, { "name": "utopia-php/logger", "version": "0.8.0", @@ -4490,16 +4606,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.22.0", + "version": "0.22.2", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030" + "reference": "f99feceab575243f3a86ee2e90cd1a6407805def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", - "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/f99feceab575243f3a86ee2e90cd1a6407805def", + "reference": "f99feceab575243f3a86ee2e90cd1a6407805def", "shasum": "" }, "require": { @@ -4535,22 +4651,22 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.22.0" + "source": "https://github.com/utopia-php/messaging/tree/0.22.2" }, - "time": "2026-04-02T04:09:19+00:00" + "time": "2026-05-14T08:51:26+00:00" }, { "name": "utopia-php/migration", - "version": "1.11.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1" + "reference": "3ee6e12af256726bddc3a0402c94535132abecc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1", - "reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/3ee6e12af256726bddc3a0402c94535132abecc6", + "reference": "3ee6e12af256726bddc3a0402c94535132abecc6", "shasum": "" }, "require": { @@ -4590,9 +4706,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.11.0" + "source": "https://github.com/utopia-php/migration/tree/1.12.0" }, - "time": "2026-05-11T08:13:06+00:00" + "time": "2026-05-14T07:30:09+00:00" }, { "name": "utopia-php/mongo", @@ -4657,26 +4773,26 @@ }, { "name": "utopia-php/platform", - "version": "1.0.0-rc1", + "version": "1.0.0-rc2", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "36c0a8b2f3d96ca056d724701a302a127111e933" + "reference": "a67e5037007ee7fdca5359ab4577b82917e55452" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/36c0a8b2f3d96ca056d724701a302a127111e933", - "reference": "36c0a8b2f3d96ca056d724701a302a127111e933", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/a67e5037007ee7fdca5359ab4577b82917e55452", + "reference": "a67e5037007ee7fdca5359ab4577b82917e55452", "shasum": "" }, "require": { "ext-json": "*", "ext-redis": "*", "php": ">=8.3", - "utopia-php/cli": "0.23.3", + "utopia-php/cli": "0.23.*", "utopia-php/http": "^2.0@RC", - "utopia-php/queue": "0.18.2", - "utopia-php/servers": "0.4.0" + "utopia-php/queue": "0.18.*", + "utopia-php/servers": "0.4.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -4702,9 +4818,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc1" + "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc2" }, - "time": "2026-05-05T15:09:27+00:00" + "time": "2026-05-15T06:19:20+00:00" }, { "name": "utopia-php/pools", @@ -4813,17 +4929,63 @@ "time": "2020-10-24T07:04:59+00:00" }, { - "name": "utopia-php/queue", - "version": "0.18.2", + "name": "utopia-php/query", + "version": "0.1.1", "source": { "type": "git", - "url": "https://github.com/utopia-php/queue.git", - "reference": "f85ca003c99ff475708c05466643d067403c0c22" + "url": "https://github.com/utopia-php/query.git", + "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22", - "reference": "f85ca003c99ff475708c05466643d067403c0c22", + "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.3", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/queue.git", + "reference": "141aad162b90728353f3aa834684b1f2affed045" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/141aad162b90728353f3aa834684b1f2affed045", + "reference": "141aad162b90728353f3aa834684b1f2affed045", "shasum": "" }, "require": { @@ -4874,9 +5036,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.18.2" + "source": "https://github.com/utopia-php/queue/tree/0.18.3" }, - "time": "2026-05-05T04:38:59+00:00" + "time": "2026-05-14T08:53:35+00:00" }, { "name": "utopia-php/registry", @@ -5030,16 +5192,16 @@ }, { "name": "utopia-php/storage", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "64e132a3768e22243eda36fe4262da22fd204f3c" + "reference": "37129cf0bfcc03210172000e4388d4d3495ae013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/64e132a3768e22243eda36fe4262da22fd204f3c", - "reference": "64e132a3768e22243eda36fe4262da22fd204f3c", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/37129cf0bfcc03210172000e4388d4d3495ae013", + "reference": "37129cf0bfcc03210172000e4388d4d3495ae013", "shasum": "" }, "require": { @@ -5076,9 +5238,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/2.0.2" + "source": "https://github.com/utopia-php/storage/tree/2.0.3" }, - "time": "2026-05-01T15:06:16+00:00" + "time": "2026-05-15T09:42:32+00:00" }, { "name": "utopia-php/system", @@ -5193,16 +5355,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.2", + "version": "0.2.3", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6" + "reference": "9770269c8ed8e6909934965fa8722103c7434c23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6", - "reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23", + "reference": "9770269c8ed8e6909934965fa8722103c7434c23", "shasum": "" }, "require": { @@ -5232,28 +5394,28 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.2" + "source": "https://github.com/utopia-php/validators/tree/0.2.3" }, - "time": "2026-04-27T16:30:24+00:00" + "time": "2026-05-14T08:05:44+00:00" }, { "name": "utopia-php/vcs", - "version": "3.2.1", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "03ccd12b75d67d29094eb760b468fddde4b6b5e5" + "reference": "2850dbe975ee69b9466ee6df385fe1679394ce78" }, "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/2850dbe975ee69b9466ee6df385fe1679394ce78", + "reference": "2850dbe975ee69b9466ee6df385fe1679394ce78", "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 +5443,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.1.0" }, - "time": "2026-05-08T10:13:53+00:00" + "time": "2026-05-14T10:04:10+00:00" }, { "name": "utopia-php/websocket", @@ -5476,16 +5638,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.28.4", + "version": "1.29.5", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "38de925e8c9e7f0f720d45187be54a291aaf696b" + "reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620" }, "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/e670edcdfb9ffcec36125b1eb3e4473dce30b620", + "reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620", "shasum": "" }, "require": { @@ -5521,9 +5683,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.5" }, - "time": "2026-05-11T13:55:49+00:00" + "time": "2026-05-15T06:49:05+00:00" }, { "name": "brianium/paratest", @@ -6630,16 +6792,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.24", + "version": "12.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" + "reference": "792c2980442dfce319226b88fa845b8b6de3b333" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/792c2980442dfce319226b88fa845b8b6de3b333", + "reference": "792c2980442dfce319226b88fa845b8b6de3b333", "shasum": "" }, "require": { @@ -6708,7 +6870,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.25" }, "funding": [ { @@ -6716,7 +6878,7 @@ "type": "other" } ], - "time": "2026-05-01T04:21:04+00:00" + "time": "2026-05-13T03:56:57+00:00" }, { "name": "sebastian/cli-parser", @@ -7701,16 +7863,16 @@ }, { "name": "symfony/console", - "version": "v8.0.9", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d" + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d", - "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d", + "url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782", "shasum": "" }, "require": { @@ -7767,7 +7929,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.9" + "source": "https://github.com/symfony/console/tree/v8.0.11" }, "funding": [ { @@ -7787,7 +7949,7 @@ "type": "tidelift" } ], - "time": "2026-04-29T15:02:55+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8121,16 +8283,16 @@ }, { "name": "symfony/process", - "version": "v8.0.8", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "url": "https://api.github.com/repos/symfony/process/zipball/26d89e459f037d2873300605d0a07e7a8ef84db0", + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0", "shasum": "" }, "require": { @@ -8162,7 +8324,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.8" + "source": "https://github.com/symfony/process/tree/v8.0.11" }, "funding": [ { @@ -8182,20 +8344,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-11T16:56:32+00:00" }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", "shasum": "" }, "require": { @@ -8252,7 +8414,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.0.11" }, "funding": [ { @@ -8272,7 +8434,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "textalk/websocket", @@ -8456,8 +8618,7 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "utopia-php/http": 5, - "utopia-php/platform": 5 + "utopia-php/http": 5 }, "prefer-stable": true, "prefer-lowest": false, 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/Database.php b/src/Appwrite/Event/Message/Database.php new file mode 100644 index 0000000000..1178dcf5c7 --- /dev/null +++ b/src/Appwrite/Event/Message/Database.php @@ -0,0 +1,51 @@ + $this->project?->getArrayCopy(), + 'user' => $this->user?->getArrayCopy(), + 'type' => $this->type, + 'table' => $this->table?->getArrayCopy(), + 'row' => $this->row?->getArrayCopy(), + 'collection' => $this->collection?->getArrayCopy(), + 'document' => $this->document?->getArrayCopy(), + 'database' => $this->database?->getArrayCopy(), + 'events' => $this->events, + ]; + } + + 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, + type: $data['type'] ?? '', + table: !empty($data['table']) ? new Document($data['table']) : null, + row: !empty($data['row']) ? new Document($data['row']) : null, + collection: !empty($data['collection']) ? new Document($data['collection']) : null, + document: !empty($data['document']) ? new Document($data['document']) : null, + database: !empty($data['database']) ? new Document($data['database']) : null, + events: $data['events'] ?? [], + ); + } +} diff --git a/src/Appwrite/Event/Message/Delete.php b/src/Appwrite/Event/Message/Delete.php new file mode 100644 index 0000000000..6866cf3f02 --- /dev/null +++ b/src/Appwrite/Event/Message/Delete.php @@ -0,0 +1,45 @@ + $this->project?->getArrayCopy(), + 'type' => $this->type, + 'document' => $this->document?->getArrayCopy(), + 'resource' => $this->resource, + 'resourceType' => $this->resourceType, + 'datetime' => $this->datetime, + 'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + project: !empty($data['project']) ? new Document($data['project']) : null, + type: $data['type'] ?? '', + document: !empty($data['document']) ? new Document($data['document']) : null, + resource: $data['resource'] ?? null, + resourceType: $data['resourceType'] ?? null, + datetime: $data['datetime'] ?? null, + hourlyUsageRetentionDatetime: $data['hourlyUsageRetentionDatetime'] ?? null, + ); + } +} 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/Database.php b/src/Appwrite/Event/Publisher/Database.php new file mode 100644 index 0000000000..09d5c33f03 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Database.php @@ -0,0 +1,45 @@ +publish($queue ?? $this->getQueueFromProject($message->project), $message); + } + + public function getSize(bool $failed = false, ?Queue $queue = null): int + { + return $this->getQueueSize($queue ?? $this->queue, $failed); + } + + private function getQueueFromProject(?Document $project): Queue + { + $database = $project?->getAttribute('database', ''); + if (empty($database)) { + return $this->queue; + } + + try { + $dsn = new DSN($database); + } catch (\InvalidArgumentException) { + $dsn = new DSN('mysql://' . $database); + } + + return new Queue($dsn->getHost()); + } +} diff --git a/src/Appwrite/Event/Publisher/Delete.php b/src/Appwrite/Event/Publisher/Delete.php new file mode 100644 index 0000000000..fb3b46c647 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Delete.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/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/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/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/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..1efc029c17 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php @@ -0,0 +1,100 @@ +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('publisherForDeletes') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $reportId, + Response $response, + Document $project, + Database $dbForPlatform, + DeletePublisher $publisherForDeletes, + 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'); + } + + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_REPORT, + document: $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/Console/Http/Scopes/Organization/XList.php b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Organization/XList.php new file mode 100644 index 0000000000..4f88df6948 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Organization/XList.php @@ -0,0 +1,69 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/console/scopes/organization') + ->desc('List organization scopes') + ->groups(['api']) + ->label('scope', 'public') + ->label('sdk', new Method( + namespace: 'console', + group: 'console', + name: 'listOrganizationScopes', + description: 'List all scopes available for organization API keys, along with a description for each scope.', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_CONSOLE_KEY_SCOPE_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(Response $response): void + { + $scopesConfig = Config::getParam('organizationScopes', []); + + $scopes = []; + foreach ($scopesConfig as $scopeId => $scope) { + $scopes[] = new Document([ + '$id' => $scopeId, + 'description' => $scope['description'] ?? '', + 'category' => $scope['category'] ?? '', + 'deprecated' => $scope['deprecated'] ?? false, + ]); + } + + $response->dynamic(new Document([ + 'total' => \count($scopes), + 'scopes' => $scopes, + ]), Response::MODEL_CONSOLE_KEY_SCOPE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php similarity index 96% rename from src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php rename to src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php index d951e93886..3e6eceb26c 100644 --- a/src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php +++ b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/console/templates/email/:templateId') + ->desc('Get email template') + ->groups(['api']) + ->label('scope', 'public') + ->label('sdk', new Method( + namespace: 'console', + group: null, + name: 'getEmailTemplate', + description: <<param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? [])) + ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes']) + ->inject('response') + ->callback($this->action(...)); + } + + public function action( + string $templateId, + string $locale, + Response $response, + ): void { + $locale = $locale ?: System::getEnv('_APP_LOCALE', 'en'); + + $localeObj = new Locale($locale); + $localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en')); + + $response->dynamic(new Document([ + 'templateId' => $templateId, + 'locale' => $locale, + 'subject' => $localeObj->getText('emails.' . $templateId . '.subject'), + 'message' => $this->getDefaultMessage($templateId, $localeObj), + 'senderName' => '', + 'senderEmail' => '', + 'replyToEmail' => '', + 'replyToName' => '', + ]), Response::MODEL_EMAIL_TEMPLATE); + } + + private function getDefaultMessage(string $templateId, Locale $localeObj): string + { + $templateConfigs = [ + 'magicSession' => [ + 'file' => 'email-magic-url.tpl', + 'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase'] + ], + 'mfaChallenge' => [ + 'file' => 'email-mfa-challenge.tpl', + 'placeholders' => ['description', 'clientInfo'] + ], + 'otpSession' => [ + 'file' => 'email-otp.tpl', + 'placeholders' => ['description', 'clientInfo', 'securityPhrase'] + ], + 'sessionAlert' => [ + 'file' => 'email-session-alert.tpl', + 'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer'] + ], + ]; + + $config = $templateConfigs[$templateId] ?? [ + 'file' => 'email-inner-base.tpl', + 'placeholders' => ['buttonText', 'body', 'footer'] + ]; + + $templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']); + $message = Template::fromString($templateString); + + foreach ($config['placeholders'] as $param) { + $escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']); + if ($templateId === 'magicSession' && $param === 'securityPhrase') { + $message->setParam('{{securityPhrase}}', ''); + continue; + } + + $message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml); + } + + $message + ->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello")) + ->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks")) + ->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature")); + + return $message->render(useContent: true); + } +} diff --git a/src/Appwrite/Platform/Modules/Console/Services/Http.php b/src/Appwrite/Platform/Modules/Console/Services/Http.php index 2540ae8e01..78b2835402 100644 --- a/src/Appwrite/Platform/Modules/Console/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Console/Services/Http.php @@ -15,7 +15,9 @@ use Appwrite\Platform\Modules\Console\Http\Redirects\Recover\Get as RedirectReco use Appwrite\Platform\Modules\Console\Http\Redirects\Register\Get as RedirectRegister; use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot; use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability; -use Appwrite\Platform\Modules\Console\Http\Scopes\Key\XList as ListKeyScopes; +use Appwrite\Platform\Modules\Console\Http\Scopes\Organization\XList as ListOrganizationScopes; +use Appwrite\Platform\Modules\Console\Http\Scopes\Project\XList as ListKeyScopes; +use Appwrite\Platform\Modules\Console\Http\Templates\Email\Get as GetEmailTemplate; use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables; use Utopia\Platform\Service; @@ -30,8 +32,10 @@ class Http extends Service $this->addAction(Web::getName(), new Web()); $this->addAction(GetVariables::getName(), new GetVariables()); + $this->addAction(GetEmailTemplate::getName(), new GetEmailTemplate()); $this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers()); $this->addAction(ListKeyScopes::getName(), new ListKeyScopes()); + $this->addAction(ListOrganizationScopes::getName(), new ListOrganizationScopes()); $this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery()); $this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index 4e5203b13f..a07a4be561 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response as UtopiaResponse; @@ -312,7 +313,7 @@ abstract class Action extends UtopiaAction }; } - protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document + protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): Document { $key = $attribute->getAttribute('key'); $type = $attribute->getAttribute('type', ''); @@ -464,20 +465,6 @@ abstract class Action extends UtopiaAction $dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence()); } - $queueForDatabase - ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) - ->setDatabase($db); - - if ($this->isCollectionsAPI()) { - $queueForDatabase - ->setDocument($attribute) - ->setCollection($collection); - } else { - $queueForDatabase - ->setRow($attribute) - ->setTable($collection); - } - $queueForEvents ->setContext('database', $db) ->setParam('databaseId', $databaseId) @@ -487,6 +474,18 @@ abstract class Action extends UtopiaAction ->setParam('columnId', $attribute->getId()) ->setContext($this->getCollectionsEventsContext(), $collection); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_CREATE_ATTRIBUTE, + database: $db, + collection: $this->isCollectionsAPI() ? $collection : null, + document: $this->isCollectionsAPI() ? $attribute : null, + table: $this->isCollectionsAPI() ? null : $collection, + row: $this->isCollectionsAPI() ? null : $attribute, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED); return $attribute; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php index 4ea85b71e6..11d3ada810 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -73,13 +73,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $min ??= \PHP_INT_MIN; $max ??= \PHP_INT_MAX; @@ -102,7 +102,7 @@ class Create extends Action 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php index a19b1626c9..475b43f569 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; @@ -68,13 +68,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -83,7 +83,7 @@ class Create extends Action 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php index 4162b50daf..7a0776751b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; @@ -69,13 +69,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -91,7 +91,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index 38b96e67bc..ff1636ae60 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -66,13 +67,13 @@ class Delete extends Action ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($db->isEmpty()) { @@ -129,20 +130,6 @@ class Delete extends Action } } - $queueForDatabase - ->setDatabase($db) - ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE); - - if ($this->isCollectionsAPI()) { - $queueForDatabase - ->setRow($attribute) - ->setTable($collection); - } else { - $queueForDatabase - ->setDocument($attribute) - ->setCollection($collection); - } - $type = $attribute->getAttribute('type'); $format = $attribute->getAttribute('format'); @@ -158,6 +145,18 @@ class Delete extends Action ->setPayload($response->output($attribute, $model)) ->setContext($this->getCollectionsEventsContext(), $collection); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_DELETE_ATTRIBUTE, + database: $db, + collection: $this->isCollectionsAPI() ? null : $collection, + document: $this->isCollectionsAPI() ? null : $attribute, + table: $this->isCollectionsAPI() ? $collection : null, + row: $this->isCollectionsAPI() ? $attribute : null, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php index 6530cdb1dd..098083bea6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; @@ -69,13 +69,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -91,7 +91,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php index fbc2d08cd1..602189e881 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -72,13 +72,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!is_null($default) && !\in_array($default, $elements, true)) { throw new Exception($this->getInvalidValueException(), 'Default value not found in elements'); @@ -99,7 +99,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php index e1585be169..a715b51b5a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -73,13 +73,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $min ??= -PHP_FLOAT_MAX; $max ??= PHP_FLOAT_MAX; @@ -102,7 +102,7 @@ class Create extends Action 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php index 8b02339252..9a142b1a86 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; @@ -69,13 +69,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $attribute = $this->createAttribute( $databaseId, @@ -91,7 +91,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php index 3d2fa68797..89aefb87e6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -73,13 +73,13 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $min ??= \PHP_INT_MIN; $max ??= \PHP_INT_MAX; @@ -104,7 +104,7 @@ class Create extends Action 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE, 'formatOptions' => ['min' => $min, 'max' => $max], - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $formatOptions = $attribute->getAttribute('formatOptions', []); if (!empty($formatOptions)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php index d2578a963f..d3f82cd109 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -69,13 +69,13 @@ class Create extends Action ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when attribute is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) { throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.'); @@ -86,7 +86,7 @@ class Create extends Action 'type' => Database::VAR_LINESTRING, 'required' => $required, 'default' => $default - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php index 2fc9de8699..90591b43fb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Longtext; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -67,7 +67,7 @@ class Create extends Action ->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') @@ -84,7 +84,7 @@ class Create extends Action bool $encrypt, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, array $plan, Authorization $authorization @@ -112,7 +112,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php index 5776e51917..0f7b386fd5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Mediumtext; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -67,7 +67,7 @@ class Create extends Action ->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') @@ -84,7 +84,7 @@ class Create extends Action bool $encrypt, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, array $plan, Authorization $authorization @@ -112,7 +112,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php index 527b4330b9..38082b46da 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -69,13 +69,13 @@ class Create extends Action ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when attribute is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) { throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.'); @@ -86,7 +86,7 @@ class Create extends Action 'type' => Database::VAR_POINT, 'required' => $required, 'default' => $default, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php index 4c3e725f3e..3063d1938a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -69,13 +69,13 @@ class Create extends Action ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when attribute is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) { throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.'); @@ -86,7 +86,7 @@ class Create extends Action 'type' => Database::VAR_POLYGON, 'required' => $required, 'default' => $default, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php index fdd40aaa8f..ace48a5c56 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -81,13 +81,13 @@ class Create extends Action ], true), 'Constraints option', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { if (!$dbForProject->getAdapter()->getSupportForRelationships()) { throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Relationships are not supported by this database.'); @@ -159,7 +159,7 @@ class Create extends Action 'twoWayKey' => $twoWayKey, 'onDelete' => $onDelete, ] - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); foreach ($attribute->getAttribute('options', []) as $k => $option) { $attribute->setAttribute($k, $option); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php index c8917c3deb..a32a3083ab 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -75,7 +75,7 @@ class Create extends Action ->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') @@ -93,7 +93,7 @@ class Create extends Action bool $encrypt, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, array $plan, Authorization $authorization @@ -134,7 +134,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php index eb6b2f9691..79968d0feb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Text; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -67,7 +67,7 @@ class Create extends Action ->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') @@ -84,7 +84,7 @@ class Create extends Action bool $encrypt, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, array $plan, Authorization $authorization @@ -112,7 +112,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php index 7ada8c7f7d..7338bdbd1d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Deprecated; @@ -69,7 +69,7 @@ class Create extends Action ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); @@ -84,7 +84,7 @@ class Create extends Action bool $array, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization ): void { @@ -96,7 +96,7 @@ class Create extends Action 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization); + ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization); $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php index 24a36725c8..89690de4e9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Varchar; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action; use Appwrite\SDK\AuthType; @@ -70,7 +70,7 @@ class Create extends Action ->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') @@ -88,7 +88,7 @@ class Create extends Action bool $encrypt, UtopiaResponse $response, Database $dbForProject, - EventDatabase $queueForDatabase, + DatabasePublisher $publisherForDatabase, Event $queueForEvents, array $plan, Authorization $authorization @@ -129,7 +129,7 @@ class Create extends Action ]), $response, $dbForProject, - $queueForDatabase, + $publisherForDatabase, $queueForEvents, $authorization ); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php index 7a5b73f7db..87171fb2fe 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -63,13 +64,13 @@ class Delete extends Action ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { @@ -89,22 +90,22 @@ class Delete extends Action $dbForDatabases = $getDatabasesDB($database); $dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence()); - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_COLLECTION) - ->setDatabase($database); - - if ($this->isCollectionsAPI()) { - $queueForDatabase->setCollection($collection); - } else { - $queueForDatabase->setTable($collection); - } - $queueForEvents ->setParam('databaseId', $databaseId) ->setContext('database', $database) ->setParam($this->getEventsParamKey(), $collection->getId()) ->setPayload($response->output($collection, $this->getResponseModel())); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_DELETE_COLLECTION, + database: $database, + collection: $this->isCollectionsAPI() ? $collection : null, + table: $this->isCollectionsAPI() ? null : $collection, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response->noContent(); } } 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/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php index 7e073c95d4..6c13a5c33c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -78,13 +79,13 @@ class Create extends Action ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -228,20 +229,6 @@ class Create extends Action $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); - $queueForDatabase - ->setType(DATABASE_TYPE_CREATE_INDEX) - ->setDatabase($db); - - if ($this->isCollectionsAPI()) { - $queueForDatabase - ->setCollection($collection) - ->setDocument($index); - } else { - $queueForDatabase - ->setTable($collection) - ->setRow($index); - } - $queueForEvents ->setContext('database', $db) ->setParam('databaseId', $databaseId) @@ -250,6 +237,18 @@ class Create extends Action ->setParam('tableId', $collection->getId()) ->setContext($this->getCollectionsEventsContext(), $collection); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_CREATE_INDEX, + database: $db, + collection: $this->isCollectionsAPI() ? $collection : null, + document: $this->isCollectionsAPI() ? $index : null, + table: $this->isCollectionsAPI() ? null : $collection, + row: $this->isCollectionsAPI() ? null : $index, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED) ->dynamic($index, $this->getResponseModel()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index dea62bfc16..82cada6e0d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -69,13 +70,13 @@ class Delete extends Action ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); } - public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void + public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void { $db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -103,20 +104,6 @@ class Delete extends Action $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_INDEX) - ->setDatabase($db); - - if ($this->isCollectionsAPI()) { - $queueForDatabase - ->setCollection($collection) - ->setDocument($index); - } else { - $queueForDatabase - ->setTable($collection) - ->setRow($index); - } - $queueForEvents ->setContext('database', $db) ->setParam('databaseId', $databaseId) @@ -126,6 +113,18 @@ class Delete extends Action ->setContext($this->getCollectionsEventsContext(), $collection) ->setPayload($response->output($index, $this->getResponseModel())); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_DELETE_INDEX, + database: $db, + collection: $this->isCollectionsAPI() ? $collection : null, + document: $this->isCollectionsAPI() ? $index : null, + table: $this->isCollectionsAPI() ? null : $collection, + row: $this->isCollectionsAPI() ? null : $index, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php index 1046d7e566..058c48d68f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases; -use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; +use Appwrite\Event\Message\Database as DatabaseMessage; +use Appwrite\Event\Publisher\Database as DatabasePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -58,12 +59,12 @@ class Delete extends Action ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->callback($this->action(...)); } - public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void + public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents): void { $database = $dbForProject->getDocument('databases', $databaseId); @@ -78,14 +79,18 @@ class Delete extends Action $dbForProject->purgeCachedDocument('databases', $database->getId()); $dbForProject->purgeCachedCollection('databases_' . $database->getSequence()); - $queueForDatabase - ->setType(DATABASE_TYPE_DELETE_DATABASE) - ->setDatabase($database); - $queueForEvents ->setParam('databaseId', $database->getId()) ->setPayload($response->output($database, UtopiaResponse::MODEL_DATABASE)); + $publisherForDatabase->enqueue(new DatabaseMessage( + project: $queueForEvents->getProject(), + user: $queueForEvents->getUser(), + type: DATABASE_TYPE_DELETE_DATABASE, + database: $database, + events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()), + )); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php index d57cebbe4a..072cb21bbc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php @@ -2,7 +2,8 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; -use Appwrite\Event\Delete as DeleteEvent; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -10,6 +11,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Response as SwooleResponse; @@ -51,11 +53,12 @@ class Delete extends Action ->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') + ->inject('project') ->callback($this->action(...)); } - public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeleteEvent $queueForDeletes): void + public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeletePublisher $publisherForDeletes, Document $project): void { $transaction = $dbForProject->getDocument('transactions', $transactionId); @@ -65,9 +68,11 @@ class Delete extends Action $dbForProject->deleteDocument('transactions', $transactionId); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $transaction, + )); $response->noContent(); } 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..fe2ad8dbae 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -3,8 +3,11 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; use Appwrite\Databases\TransactionState; -use Appwrite\Event\Delete; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Message\Func as FunctionMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; +use Appwrite\Event\Publisher\Func as FunctionPublisher; use Appwrite\Extend\Exception; use Appwrite\Functions\EventProcessor; use Appwrite\SDK\AuthType; @@ -73,11 +76,11 @@ class Update extends Action ->inject('getDatabasesDB') ->inject('user') ->inject('transactionState') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('usage') ->inject('queueForRealtime') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('queueForWebhooks') ->inject('authorization') ->inject('eventProcessor') @@ -93,11 +96,11 @@ class Update extends Action * @param callable $getDatabasesDB * @param User $user * @param TransactionState $transactionState - * @param Delete $queueForDeletes + * @param DeletePublisher $publisherForDeletes * @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 +111,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, DeletePublisher $publisherForDeletes, 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'); @@ -154,9 +157,11 @@ class Update extends Action new Document(['status' => 'committed']) )); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $transaction, + )); $response ->setStatusCode(SwooleResponse::STATUS_CODE_OK) @@ -293,9 +298,11 @@ class Update extends Action new Document(['status' => 'committed']) )); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $transaction, + )); } catch (NotFoundException $e) { $authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', @@ -461,7 +468,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 +495,6 @@ class Update extends Action $queueForEvents->reset(); $queueForRealtime->reset(); - $queueForFunctions->reset(); $queueForWebhooks->reset(); } } @@ -492,9 +506,11 @@ class Update extends Action new Document(['status' => 'failed']) )); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($transaction); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $transaction, + )); } $response diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php index d698b40203..043f74998d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php @@ -54,7 +54,7 @@ class Delete extends CollectionDelete ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); 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/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php index dc3ce34605..637255f16a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php @@ -65,7 +65,7 @@ class Create extends IndexCreate ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php index d4464f171d..1e3c012b4f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php @@ -59,7 +59,7 @@ class Delete extends IndexDelete ->param('key', '', new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php index 1708656c98..5e63ab8a7f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php @@ -48,7 +48,7 @@ class Delete extends DatabaseDelete ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('usage') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php index 036f2e9600..94ff3fa214 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php @@ -49,7 +49,8 @@ class Delete extends TransactionsDelete ->param('transactionId', '', new UID(), 'Transaction ID.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') + ->inject('project') ->callback($this->action(...)); } } 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..1b9cdee137 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php @@ -56,11 +56,11 @@ class Update extends TransactionsUpdate ->inject('getDatabasesDB') ->inject('user') ->inject('transactionState') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->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/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php index 7873d369e6..70dc8430f2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php @@ -48,7 +48,7 @@ class Delete extends DatabaseDelete ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->callback($this->action(...)); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php index 1d32c6bad9..9d882e09a6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php @@ -62,7 +62,7 @@ class Create extends BigIntCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php index 10cd65bc98..334c8b5124 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php @@ -59,7 +59,7 @@ class Create extends BooleanCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php index 64e73e310e..922e071f35 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php @@ -60,7 +60,7 @@ class Create extends DatetimeCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php index f4d606637d..8e0abf211f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php @@ -57,7 +57,7 @@ class Delete extends AttributesDelete ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php index d0b2ed3e4b..072e334b4b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php @@ -60,7 +60,7 @@ class Create extends EmailCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php index e58ae115fc..9d24f310bd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php @@ -62,7 +62,7 @@ class Create extends EnumCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php index b8e81820aa..d68b3a4921 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php @@ -62,7 +62,7 @@ class Create extends FloatCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php index c2faec9aeb..ff5828e749 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php @@ -60,7 +60,7 @@ class Create extends IPCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php index 1a965c19dc..dec399cdb2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php @@ -62,7 +62,7 @@ class Create extends IntegerCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php index c2f480d5d0..71548c74da 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php @@ -59,7 +59,7 @@ class Create extends LineCreate ->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when column is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php index 8e2dbd911d..ec0f633400 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php @@ -60,7 +60,7 @@ class Create extends LongtextCreate ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php index f0b8099f02..2728caa58f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php @@ -60,7 +60,7 @@ class Create extends MediumtextCreate ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php index 138ee482c3..601e19299b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php @@ -59,7 +59,7 @@ class Create extends PointCreate ->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when column is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php index a03a34f310..36972d5da2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php @@ -59,7 +59,7 @@ class Create extends PolygonCreate ->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when column is required.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php index 87544926fe..414cf03b3d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php @@ -71,7 +71,7 @@ class Create extends RelationshipCreate ], true), 'Constraints option', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php index 17f60f61c1..8151b3e8da 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php @@ -69,7 +69,7 @@ class Create extends StringCreate ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php index a8fde7d271..bffdc96001 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php @@ -60,7 +60,7 @@ class Create extends TextCreate ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php index 19b33594b7..2edf4a62f6 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php @@ -60,7 +60,7 @@ class Create extends URLCreate ->param('array', false, new Boolean(), 'Is column an array?', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php index 7595f16c45..307a1fd5e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php @@ -63,7 +63,7 @@ class Create extends VarcharCreate ->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('plan') ->inject('authorization') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php index 97c5465fe3..3a6d6666f2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php @@ -55,7 +55,7 @@ class Delete extends CollectionDelete ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php index d377bed184..77496fea59 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php @@ -65,7 +65,7 @@ class Create extends IndexCreate ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php index ca7e4fc2da..6cd5cfe78f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php @@ -60,7 +60,7 @@ class Delete extends IndexDelete ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); 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/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php index 9ee85ff153..988bfc3d1d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php @@ -50,7 +50,8 @@ class Delete extends TransactionsDelete ->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') + ->inject('project') ->callback($this->action(...)); } } 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..bd06f475b2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php @@ -57,11 +57,11 @@ class Update extends TransactionsUpdate ->inject('getDatabasesDB') ->inject('user') ->inject('transactionState') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->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/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php index f1188868aa..6ee83b2530 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php @@ -54,7 +54,7 @@ class Delete extends CollectionDelete ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); 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/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php index a535dd5724..bba7ee0579 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php @@ -65,7 +65,7 @@ class Create extends IndexCreate ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php index 5c7fc47ee0..67e13dd26a 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php @@ -59,7 +59,7 @@ class Delete extends IndexDelete ->param('key', '', new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php index c9d36904a9..a33eedccd5 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php @@ -47,7 +47,7 @@ class Delete extends DatabaseDelete ->param('databaseId', '', new UID(), 'Database ID.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('queueForEvents') ->inject('usage') ->callback($this->action(...)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php index 0ac2caecba..2de71fc904 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php @@ -49,7 +49,8 @@ class Delete extends TransactionsDelete ->param('transactionId', '', new UID(), 'Transaction ID.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') + ->inject('project') ->callback($this->action(...)); } } 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..cebfcb42e8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php @@ -56,11 +56,11 @@ class Update extends TransactionsUpdate ->inject('getDatabasesDB') ->inject('user') ->inject('transactionState') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('usage') ->inject('queueForRealtime') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('queueForWebhooks') ->inject('authorization') ->inject('eventProcessor') diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 39902aea53..ee8494b382 100644 --- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Databases\Workers; +use Appwrite\Event\Message\Database as DatabaseMessage; use Appwrite\Event\Realtime; use Exception; use Utopia\Console; @@ -60,10 +61,11 @@ class Databases extends Action throw new Exception('Missing payload'); } - $type = $payload['type']; - $document = new Document($payload['row'] ?? $payload['document'] ?? []); - $collection = new Document($payload['table'] ?? $payload['collection'] ?? []); - $database = new Document($payload['database'] ?? []); + $databaseMessage = DatabaseMessage::fromArray($payload); + $type = $databaseMessage->type; + $document = $databaseMessage->row ?? $databaseMessage->document ?? new Document(); + $collection = $databaseMessage->table ?? $databaseMessage->collection ?? new Document(); + $database = $databaseMessage->database ?? new Document(); /** * @var Database $dbForDatabases */ diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index 57c465faef..9af5491598 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -21,6 +21,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; +use Utopia\Lock\Exception\Contention as LockContention; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; @@ -92,6 +93,7 @@ class Create extends Action ->inject('plan') ->inject('authorization') ->inject('platform') + ->inject('locks') ->callback($this->action(...)); } @@ -111,7 +113,8 @@ class Create extends Action BuildPublisher $publisherForBuilds, array $plan, Authorization $authorization, - array $platform + array $platform, + callable $locks ) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; @@ -193,20 +196,38 @@ class Create extends Action // Save to storage $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); - $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $lockKey = 'functions:deployment:' . $project->getId() . ':' . $functionId . ':' . $deploymentId; $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; - if (!$deployment->isEmpty()) { - $chunks = $deployment->getAttribute('sourceChunksTotal', 1); - $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); - $metadata = $deployment->getAttribute('sourceMetadata', []); + $completed = false; - if ($uploaded === $chunks) { - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - return; - } + try { + $locks($lockKey, 600, function () use (&$chunks, $dbForProject, $deploymentId, &$metadata, &$completed, $response): void { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if (!$deployment->isEmpty()) { + $chunks = $deployment->getAttribute('sourceChunksTotal', 1); + $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); + $metadata = $deployment->getAttribute('sourceMetadata', []); + + if ($uploaded === $chunks) { + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + $completed = true; + return; + } + } + }, timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.'); + } + + if ($completed) { + return; } $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); @@ -217,118 +238,144 @@ class Create extends Action $type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual'; - if ($chunksUploaded === $chunks) { - if ($activate) { - // Remove deploy for all other deployments. - $activeDeployments = $dbForProject->find('deployments', [ - Query::equal('activate', [true]), - Query::equal('resourceId', [$functionId]), - Query::equal('resourceType', ['functions']) - ]); + try { + $locks($lockKey, 600, function () use ($activate, &$chunks, $chunksUploaded, $commands, $dbForProject, $deploymentId, $deviceForFunctions, $entrypoint, $fileSize, &$function, $functionId, $path, &$metadata, $platform, $project, $publisherForBuilds, $queueForEvents, $response, $type): void { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $uploaded = 0; - foreach ($activeDeployments as $activeDeployment) { - $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([ - 'activate' => false, - ])); + if (!$deployment->isEmpty()) { + $chunks = $deployment->getAttribute('sourceChunksTotal', 1); + $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); + $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); + + if ($uploaded === $chunks) { + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + return; + } } - } - $fileSize = $deviceForFunctions->getFileSize($path); + $chunksUploaded = max($uploaded, $chunksUploaded); - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getSequence(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'entrypoint' => $entrypoint, - 'buildCommands' => $commands, - 'startCommand' => $function->getAttribute('startCommand', ''), - 'sourcePath' => $path, - 'sourceSize' => $fileSize, - 'totalSize' => $fileSize, - 'sourceChunksTotal' => $chunks, - 'sourceChunksUploaded' => $chunksUploaded, - 'activate' => $activate, - 'sourceMetadata' => $metadata, - 'type' => $type - ])); + if ($chunksUploaded === $chunks && $uploaded < $chunks) { + if ($activate) { + // Remove deploy for all other deployments. + $activeDeployments = $dbForProject->find('deployments', [ + Query::equal('activate', [true]), + Query::equal('resourceId', [$functionId]), + Query::equal('resourceType', ['functions']) + ]); - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ - 'latestDeploymentId' => $deployment->getId(), - 'latestDeploymentInternalId' => $deployment->getSequence(), - 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), - 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), - ])); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ - 'sourceSize' => $fileSize, - 'sourceChunksUploaded' => $chunksUploaded, - 'sourceMetadata' => $metadata, - ])); - } + foreach ($activeDeployments as $activeDeployment) { + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([ + 'activate' => false, + ])); + } + } - // Start the build - $publisherForBuilds->enqueue(new BuildMessage( - project: $project, - resource: $function, - deployment: $deployment, - type: BUILD_TYPE_DEPLOYMENT, - platform: $platform, - )); - } else { - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getSequence(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'entrypoint' => $entrypoint, - 'buildCommands' => $commands, - 'startCommand' => $function->getAttribute('startCommand', ''), - 'sourcePath' => $path, - 'sourceSize' => $fileSize, - 'totalSize' => $fileSize, - 'sourceChunksTotal' => $chunks, - 'sourceChunksUploaded' => $chunksUploaded, - 'activate' => $activate, - 'sourceMetadata' => $metadata, - 'type' => $type - ])); + $fileSize = $deviceForFunctions->getFileSize($path); - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ - 'latestDeploymentId' => $deployment->getId(), - 'latestDeploymentInternalId' => $deployment->getSequence(), - 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), - 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), - ])); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ - 'sourceChunksUploaded' => $chunksUploaded, - 'sourceMetadata' => $metadata, - ])); - } + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getSequence(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'entrypoint' => $entrypoint, + 'buildCommands' => $commands, + 'startCommand' => $function->getAttribute('startCommand', ''), + 'sourcePath' => $path, + 'sourceSize' => $fileSize, + 'totalSize' => $fileSize, + 'sourceChunksTotal' => $chunks, + 'sourceChunksUploaded' => $chunksUploaded, + 'activate' => $activate, + 'sourceMetadata' => $metadata, + 'type' => $type + ])); + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); + } + + // Start the build + $publisherForBuilds->enqueue(new BuildMessage( + project: $project, + resource: $function, + deployment: $deployment, + type: BUILD_TYPE_DEPLOYMENT, + platform: $platform, + )); + } else { + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getSequence(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'entrypoint' => $entrypoint, + 'buildCommands' => $commands, + 'startCommand' => $function->getAttribute('startCommand', ''), + 'sourcePath' => $path, + 'sourceSize' => $fileSize, + 'totalSize' => $fileSize, + 'sourceChunksTotal' => $chunks, + 'sourceChunksUploaded' => $chunksUploaded, + 'activate' => $activate, + 'sourceMetadata' => $metadata, + 'type' => $type + ])); + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); + } + } + + $metadata = null; + + if ($chunksUploaded === $chunks) { + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + } + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + }, timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.'); } - - $metadata = null; - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php index 3d75919eb8..be4437ffe3 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Functions\Http\Deployments; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -59,7 +60,7 @@ class Delete extends Action ->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('deviceForFunctions') ->callback($this->action(...)); @@ -70,7 +71,7 @@ class Delete extends Action string $deploymentId, Response $response, Database $dbForProject, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents, Device $deviceForFunctions ) { @@ -128,9 +129,11 @@ class Delete extends Action ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($deployment); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $deployment, + )); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 7e86176bc9..82e5602479 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -3,9 +3,11 @@ 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\Delete as DeleteMessage; +use Appwrite\Event\Message\Func as FunctionMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; +use Appwrite\Event\Publisher\Func as FunctionPublisher; use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Functions\Validator\Headers; @@ -95,14 +97,14 @@ class Create extends Base ->inject('user') ->inject('queueForEvents') ->inject('usage') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('geoRecord') ->inject('store') ->inject('proofForToken') ->inject('executor') ->inject('platform') ->inject('authorization') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('executionsRetentionCount') ->callback($this->action(...)); } @@ -123,14 +125,14 @@ class Create extends Base User $user, Event $queueForEvents, Context $usage, - Func $queueForFunctions, + FunctionPublisher $publisherForFunctions, GeoRecord $geoRecord, Store $store, Token $proofForToken, Executor $executor, array $platform, Authorization $authorization, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, int $executionsRetentionCount, ) { $async = \strval($async) === 'true' || \strval($async) === '1'; @@ -288,20 +290,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, @@ -332,12 +333,12 @@ class Create extends Base } if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { - $queueForDeletes - ->setProject($project) - ->setResource($function->getSequence()) - ->setResourceType(RESOURCE_TYPE_FUNCTIONS) - ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_EXECUTIONS_LIMIT, + resource: (string) $function->getSequence(), + resourceType: RESOURCE_TYPE_FUNCTIONS, + )); } $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); @@ -523,12 +524,12 @@ class Create extends Base } if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) { - $queueForDeletes - ->setProject($project) - ->setResource($function->getSequence()) - ->setResourceType(RESOURCE_TYPE_FUNCTIONS) - ->setType(DELETE_TYPE_EXECUTIONS_LIMIT) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_EXECUTIONS_LIMIT, + resource: (string) $function->getSequence(), + resourceType: RESOURCE_TYPE_FUNCTIONS, + )); } $response 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/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php index fb45cee82f..1517ee7793 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Functions\Http\Functions; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -59,7 +60,7 @@ class Delete extends Base ->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('authorization') @@ -70,7 +71,7 @@ class Delete extends Base string $functionId, Response $response, Database $dbForProject, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents, Database $dbForPlatform, Authorization $authorization @@ -97,9 +98,11 @@ class Delete extends Base ]))); } - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($function); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $function, + )); $queueForEvents->setParam('functionId', $function->getId()); 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/Databases/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php index 213bd8b36c..3bd42b64c6 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Databases; -use Appwrite\Event\Database; +use Appwrite\Event\Publisher\Database; use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Document; +use Utopia\Queue\Queue; use Utopia\Validator\Integer; use Utopia\Validator\Text; @@ -44,15 +45,15 @@ class Get extends Base )) ->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true) ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) - ->inject('queueForDatabase') + ->inject('publisherForDatabase') ->inject('response') ->callback($this->action(...)); } - public function action(string $name, int|string $threshold, Database $queueForDatabase, Response $response): void + public function action(string $name, int|string $threshold, Database $publisherForDatabase, Response $response): void { $threshold = (int) $threshold; - $size = $queueForDatabase->setQueue($name)->getSize(); + $size = $publisherForDatabase->getSize(queue: new Queue($name)); $this->assertQueueThreshold($size, $threshold); diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php index 816583fc47..c1bcc900e0 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Deletes; -use Appwrite\Event\Delete; +use Appwrite\Event\Publisher\Delete; 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('queueForDeletes') + ->inject('publisherForDeletes') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Delete $queueForDeletes, Response $response): void + public function action(int|string $threshold, Delete $publisherForDeletes, Response $response): void { $threshold = (int) $threshold; - $size = $queueForDeletes->getSize(); + $size = $publisherForDeletes->getSize(); $this->assertQueueThreshold($size, $threshold); 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..d3b760d01b 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 @@ -2,13 +2,13 @@ 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\Database as DatabasePublisher; +use Appwrite\Event\Publisher\Delete as DeletePublisher; +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; @@ -74,11 +74,11 @@ class Get extends Base ]), 'The name of the queue') ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('response') - ->inject('queueForDatabase') - ->inject('queueForDeletes') + ->inject('publisherForDatabase') + ->inject('publisherForDeletes') ->inject('publisherForAudits') ->inject('publisherForMails') - ->inject('queueForFunctions') + ->inject('publisherForFunctions') ->inject('publisherForStatsResources') ->inject('publisherForUsage') ->inject('queueForWebhooks') @@ -94,11 +94,11 @@ class Get extends Base string $name, int|string $threshold, Response $response, - Database $queueForDatabase, - Delete $queueForDeletes, + DatabasePublisher $publisherForDatabase, + DeletePublisher $publisherForDeletes, Audit $publisherForAudits, MailPublisher $publisherForMails, - Func $queueForFunctions, + FunctionPublisher $publisherForFunctions, StatsResourcesPublisher $publisherForStatsResources, UsagePublisher $publisherForUsage, Webhook $queueForWebhooks, @@ -111,11 +111,11 @@ class Get extends Base $threshold = (int) $threshold; $queue = match ($name) { - System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase, - System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes, + System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $publisherForDatabase, + System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $publisherForDeletes, 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/AuthMethods/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php index 0d1cd83203..5335036cde 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php @@ -31,7 +31,7 @@ class Update extends Action ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) ->setHttpPath('/v1/project/auth-methods/:methodId') ->httpAlias('/v1/projects/:projectId/auth/:methodId') - ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.') + ->desc('Update project auth method status') ->groups(['api', 'project']) ->label('scope', 'project.write') ->label('event', 'authMethod.[methodId].update') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php index 4b26557ca9..201061dd62 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php @@ -2,7 +2,8 @@ namespace Appwrite\Platform\Modules\Project\Http\Project; -use Appwrite\Event\Delete as DeleteQueue; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -53,7 +54,7 @@ class Delete extends Action )) ->inject('response') ->inject('dbForPlatform') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('authorization') ->inject('project') ->callback($this->action(...)); @@ -62,19 +63,20 @@ class Delete extends Action public function action( Response $response, Database $dbForPlatform, - DeleteQueue $queueForDeletes, + DeletePublisher $publisherForDeletes, Authorization $authorization, Document $project, ) { - $queueForDeletes - ->setProject($project) - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($project); - if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId()))) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB'); } + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $project, + )); + $response->noContent(); } } diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index d36d0e9005..3fe9f63d9e 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -141,19 +141,19 @@ class Http extends Service $this->addAction(UpdateKey::getName(), new UpdateKey()); // Platforms - $this->addAction(DeletePlatform::getName(), new DeletePlatform()); - $this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform()); - $this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform()); - $this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform()); - $this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform()); - $this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform()); + $this->addAction(ListPlatforms::getName(), new ListPlatforms()); + $this->addAction(GetPlatform::getName(), new GetPlatform()); $this->addAction(CreateWebPlatform::getName(), new CreateWebPlatform()); $this->addAction(CreateApplePlatform::getName(), new CreateApplePlatform()); $this->addAction(CreateAndroidPlatform::getName(), new CreateAndroidPlatform()); $this->addAction(CreateWindowsPlatform::getName(), new CreateWindowsPlatform()); $this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform()); - $this->addAction(GetPlatform::getName(), new GetPlatform()); - $this->addAction(ListPlatforms::getName(), new ListPlatforms()); + $this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform()); + $this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform()); + $this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform()); + $this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform()); + $this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform()); + $this->addAction(DeletePlatform::getName(), new DeletePlatform()); // Mock Phones $this->addAction(CreateMockPhone::getName(), new CreateMockPhone()); diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 8baf54c790..f2ffc58568 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -5,7 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\DNS as ValidatorDNS; use Appwrite\Platform\Action as PlatformAction; +use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate; +use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\DNS\Message\Record; use Utopia\Domains\Domain; use Utopia\Logger\Log; @@ -20,6 +24,57 @@ class Action extends PlatformAction { } + protected function createRule(Document $rule, Database $dbForPlatform, Authorization $authorization): Document + { + try { + return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); + } catch (Duplicate) { + if (!$this->deleteOrphanedRule($rule, $dbForPlatform, $authorization)) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + } + + try { + return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); + } catch (Duplicate) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + } + + private function deleteOrphanedRule(Document $rule, Database $dbForPlatform, Authorization $authorization): bool + { + $existingRule = $authorization->skip(function () use ($rule, $dbForPlatform) { + $existingRule = $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$rule->getAttribute('domain', '')]), + ]); + if (!$existingRule->isEmpty()) { + return $existingRule; + } + + return $dbForPlatform->getDocument('rules', $rule->getId()); + }); + + if ( + $existingRule->isEmpty() || + $existingRule->getAttribute('domain', '') !== $rule->getAttribute('domain', '') + ) { + return false; + } + + $projectId = $existingRule->getAttribute('projectId', ''); + if (empty($projectId)) { + return false; + } + + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + if (!$project->isEmpty()) { + return false; + } + + $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $existingRule->getId())); + return true; + } + /** * Ensures domain is not in the deny list and is a valid domain * diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index 6f2e40d13f..9431d24cde 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Logger\Log; @@ -120,11 +119,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php index 29751ff20a..991b8eb006 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -57,7 +58,7 @@ class Delete extends Action ->inject('response') ->inject('project') ->inject('dbForPlatform') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('authorization') ->callback($this->action(...)); @@ -68,7 +69,7 @@ class Delete extends Action Response $response, Document $project, Database $dbForPlatform, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents, Authorization $authorization, ) { @@ -80,9 +81,11 @@ class Delete extends Action $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId())); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($rule); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $rule, + )); $queueForEvents->setParam('ruleId', $rule->getId()); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index c68574fefe..7cc8b5e59e 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -142,11 +141,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index f55405bb48..e8167b44a0 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -149,11 +148,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index 7da9a11636..ca45d73e13 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -142,11 +141,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 63ed776709..d27755d106 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -21,6 +21,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; +use Utopia\Lock\Exception\Contention as LockContention; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; @@ -90,6 +91,7 @@ class Create extends Action ->inject('plan') ->inject('authorization') ->inject('platform') + ->inject('locks') ->callback($this->action(...)); } @@ -112,6 +114,7 @@ class Create extends Action array $plan, Authorization $authorization, array $platform, + callable $locks, ) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; @@ -193,20 +196,38 @@ class Create extends Action // Save to storage $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); $path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); - $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $lockKey = 'sites:deployment:' . $project->getId() . ':' . $siteId . ':' . $deploymentId; $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; - if (!$deployment->isEmpty()) { - $chunks = $deployment->getAttribute('sourceChunksTotal', 1); - $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); - $metadata = $deployment->getAttribute('sourceMetadata', []); + $completed = false; - if ($uploaded === $chunks) { - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - return; - } + try { + $locks($lockKey, 600, function () use (&$chunks, $dbForProject, $deploymentId, &$metadata, &$completed, $response): void { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if (!$deployment->isEmpty()) { + $chunks = $deployment->getAttribute('sourceChunksTotal', 1); + $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); + $metadata = $deployment->getAttribute('sourceMetadata', []); + + if ($uploaded === $chunks) { + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + $completed = true; + return; + } + } + }, timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.'); + } + + if ($completed) { + return; } $chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata); @@ -225,184 +246,208 @@ class Create extends Action $commands[] = $buildCommand; } - if ($chunksUploaded === $chunks) { - if ($activate) { - // Remove deploy for all other deployments. - $activeDeployments = $dbForProject->find('deployments', [ - Query::equal('activate', [true]), - Query::equal('resourceId', [$siteId]), - Query::equal('resourceType', ['sites']) - ]); + try { + $locks($lockKey, 600, function () use ($activate, $authorization, $commands, &$chunks, $chunksUploaded, $dbForPlatform, $dbForProject, $deploymentId, $deviceForSites, $fileSize, &$metadata, $outputDirectory, $path, $platform, $project, $publisherForBuilds, $queueForEvents, $response, &$site, $siteId, $type): void { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $uploaded = 0; - foreach ($activeDeployments as $activeDeployment) { - $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false])); + if (!$deployment->isEmpty()) { + $chunks = $deployment->getAttribute('sourceChunksTotal', 1); + $uploaded = $deployment->getAttribute('sourceChunksUploaded', 0); + $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); + + if ($uploaded === $chunks) { + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + return; + } } - } - $fileSize = $deviceForSites->getFileSize($path); + $chunksUploaded = max($uploaded, $chunksUploaded); - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $site->getSequence(), - 'resourceId' => $site->getId(), - 'resourceType' => 'sites', - 'buildCommands' => \implode(' && ', $commands), - 'startCommand' => $site->getAttribute('startCommand', ''), - 'buildOutput' => $outputDirectory, - 'adapter' => $site->getAttribute('adapter', ''), - 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'sourcePath' => $path, - 'sourceSize' => $fileSize, - 'totalSize' => $fileSize, - 'sourceChunksTotal' => $chunks, - 'sourceChunksUploaded' => $chunksUploaded, - 'activate' => $activate, - 'sourceMetadata' => $metadata, - 'type' => $type, - ])); + if ($chunksUploaded === $chunks && $uploaded < $chunks) { + if ($activate) { + // Remove deploy for all other deployments. + $activeDeployments = $dbForProject->find('deployments', [ + Query::equal('activate', [true]), + Query::equal('resourceId', [$siteId]), + Query::equal('resourceType', ['sites']) + ]); - $site = $site - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), new Document([ - 'latestDeploymentId' => $deployment->getId(), - 'latestDeploymentInternalId' => $deployment->getSequence(), - 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), - 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), - ])); + foreach ($activeDeployments as $activeDeployment) { + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false])); + } + } - $sitesDomain = $platform['sitesDomain']; - $domain = ID::unique() . "." . $sitesDomain; + $fileSize = $deviceForSites->getFileSize($path); - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $ruleId = $isMd5 ? md5($domain) : ID::unique(); + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $site->getSequence(), + 'resourceId' => $site->getId(), + 'resourceType' => 'sites', + 'buildCommands' => \implode(' && ', $commands), + 'startCommand' => $site->getAttribute('startCommand', ''), + 'buildOutput' => $outputDirectory, + 'adapter' => $site->getAttribute('adapter', ''), + 'fallbackFile' => $site->getAttribute('fallbackFile', ''), + 'sourcePath' => $path, + 'sourceSize' => $fileSize, + 'totalSize' => $fileSize, + 'sourceChunksTotal' => $chunks, + 'sourceChunksUploaded' => $chunksUploaded, + 'activate' => $activate, + 'sourceMetadata' => $metadata, + 'type' => $type, + ])); - $authorization->skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), - 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $site->getId(), - 'deploymentResourceInternalId' => $site->getSequence(), - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ - 'sourceSize' => $fileSize, - 'sourceChunksUploaded' => $chunksUploaded, - 'sourceMetadata' => $metadata, - ])); - } + $site = $site + ->setAttribute('latestDeploymentId', $deployment->getId()) + ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) + ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) + ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); - // Start the build - $publisherForBuilds->enqueue(new BuildMessage( - project: $project, - resource: $site, - deployment: $deployment, - type: BUILD_TYPE_DEPLOYMENT, - platform: $platform, - )); - } else { - if ($deployment->isEmpty()) { - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $site->getSequence(), - 'resourceId' => $site->getId(), - 'resourceType' => 'sites', - 'buildCommands' => \implode(' && ', $commands), - 'startCommand' => $site->getAttribute('startCommand', ''), - 'buildOutput' => $outputDirectory, - 'adapter' => $site->getAttribute('adapter', ''), - 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'sourcePath' => $path, - 'sourceSize' => $fileSize, - 'totalSize' => $fileSize, - 'sourceChunksTotal' => $chunks, - 'sourceChunksUploaded' => $chunksUploaded, - 'activate' => $activate, - 'sourceMetadata' => $metadata, - 'type' => $type, - ])); + $sitesDomain = $platform['sitesDomain']; + $domain = ID::unique() . "." . $sitesDomain; - $site = $site - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), new Document([ - 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), - 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), - 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), - 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), - ])); + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $ruleId = $isMd5 ? md5($domain) : ID::unique(); - $sitesDomain = $platform['sitesDomain']; - $domain = ID::unique() . "." . $sitesDomain; - $ruleId = md5($domain); - $authorization->skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), - 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $site->getId(), - 'deploymentResourceInternalId' => $site->getSequence(), - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ - 'sourceChunksUploaded' => $chunksUploaded, - 'sourceMetadata' => $metadata, - ])); - } + $authorization->skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), + 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $site->getId(), + 'deploymentResourceInternalId' => $site->getSequence(), + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); + } + + // Start the build + $publisherForBuilds->enqueue(new BuildMessage( + project: $project, + resource: $site, + deployment: $deployment, + type: BUILD_TYPE_DEPLOYMENT, + platform: $platform, + )); + } else { + if ($deployment->isEmpty()) { + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $site->getSequence(), + 'resourceId' => $site->getId(), + 'resourceType' => 'sites', + 'buildCommands' => \implode(' && ', $commands), + 'startCommand' => $site->getAttribute('startCommand', ''), + 'buildOutput' => $outputDirectory, + 'adapter' => $site->getAttribute('adapter', ''), + 'fallbackFile' => $site->getAttribute('fallbackFile', ''), + 'sourcePath' => $path, + 'sourceSize' => $fileSize, + 'totalSize' => $fileSize, + 'sourceChunksTotal' => $chunks, + 'sourceChunksUploaded' => $chunksUploaded, + 'activate' => $activate, + 'sourceMetadata' => $metadata, + 'type' => $type, + ])); + + $site = $site + ->setAttribute('latestDeploymentId', $deployment->getId()) + ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) + ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) + ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); + + $sitesDomain = $platform['sitesDomain']; + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + $authorization->skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), + 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $site->getId(), + 'deploymentResourceInternalId' => $site->getSequence(), + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + } else { + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); + } + } + + $metadata = null; + + if ($chunksUploaded === $chunks) { + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + } + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + }, timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.'); } - - - - $metadata = null; - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index efea79395f..b50e9b54f4 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Sites\Http\Deployments; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -59,7 +60,7 @@ class Delete extends Action ->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('deviceForSites') ->callback($this->action(...)); @@ -70,7 +71,7 @@ class Delete extends Action string $deploymentId, Response $response, Database $dbForProject, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents, Device $deviceForSites ) { @@ -130,9 +131,11 @@ class Delete extends Action ->setParam('siteId', $site->getId()) ->setParam('deploymentId', $deployment->getId()); - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($deployment); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $deployment, + )); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php index ebc192b6e6..50b070d098 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Sites\Http\Sites; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -56,7 +57,7 @@ class Delete extends Base ->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->callback($this->action(...)); } @@ -65,7 +66,7 @@ class Delete extends Base string $siteId, Response $response, Database $dbForProject, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents ) { $site = $dbForProject->getDocument('sites', $siteId); @@ -78,9 +79,11 @@ class Delete extends Base throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove site from DB'); } - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($site); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $site, + )); $queueForEvents->setParam('siteId', $site->getId()); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Delete.php index 9523f55e12..2581a2163d 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Delete.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Storage\Http\Buckets; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -53,7 +54,7 @@ class Delete extends Action ->param('bucketId', '', new UID(), 'Bucket unique ID.') ->inject('response') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->callback($this->action(...)); } @@ -62,7 +63,7 @@ class Delete extends Action string $bucketId, Response $response, Database $dbForProject, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Event $queueForEvents ) { $bucket = $dbForProject->getDocument('buckets', $bucketId); @@ -75,9 +76,11 @@ class Delete extends Action throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove bucket from DB'); } - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($bucket); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_DOCUMENT, + document: $bucket, + )); $queueForEvents ->setParam('bucketId', $bucket->getId()) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index 2ce5ef97f5..8530475f0c 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\UID; use Utopia\Http\Adapter\Swoole\Request; +use Utopia\Lock\Exception\Contention as LockContention; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; @@ -86,12 +87,13 @@ class Create extends Action ->inject('request') ->inject('response') ->inject('dbForProject') + ->inject('project') ->inject('user') ->inject('queueForEvents') - ->inject('mode') ->inject('deviceForFiles') ->inject('deviceForLocal') ->inject('authorization') + ->inject('locks') ->callback($this->action(...)); } @@ -103,12 +105,13 @@ class Create extends Action Request $request, Response $response, Database $dbForProject, + Document $project, User $user, Event $queueForEvents, - string $mode, Device $deviceForFiles, Device $deviceForLocal, - Authorization $authorization + Authorization $authorization, + callable $locks ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -234,189 +237,242 @@ class Create extends Action $path = $deviceForFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root - $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); + $lockKey = 'storage:file:' . $project->getId() . ':' . $bucket->getId() . ':' . $fileId; $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; - if (!$file->isEmpty()) { - $chunks = $file->getAttribute('chunksTotal', 1); - $uploaded = $file->getAttribute('chunksUploaded', 0); - $metadata = $file->getAttribute('metadata', []); + $completed = false; - if ($uploaded === $chunks) { - if (empty($contentRange)) { - throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + $mergeUploadMetadata = function (array $stored, array $current): array { + $merged = \array_merge($stored, $current); + + if (isset($stored['parts']) || isset($current['parts'])) { + $parts = $stored['parts'] ?? []; + foreach (($current['parts'] ?? []) as $part => $value) { + $parts[(int) $part] = $value; + } + \ksort($parts); + + $merged['parts'] = $parts; + $merged['chunks'] = \count($parts); + } + + return $merged; + }; + + try { + $locks($lockKey, 600, function () use ($bucket, &$chunks, $contentRange, $dbForProject, $deviceForFiles, $fileId, $fileName, $fileSize, &$metadata, $path, $permissions, $response, &$completed): void { + $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); + if (!$file->isEmpty()) { + $chunks = $file->getAttribute('chunksTotal', 1); + $uploaded = $file->getAttribute('chunksUploaded', 0); + $metadata = $file->getAttribute('metadata', []); + + if ($uploaded === $chunks) { + if (empty($contentRange)) { + throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($file, Response::MODEL_FILE); + + $completed = true; + return; + } } - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic($file, Response::MODEL_FILE); - return; - } + if ($file->isEmpty()) { + $deviceForFiles->prepareUpload($path, $metadata['content_type'] ?? '', $chunks, $metadata); + + if (!empty($contentRange)) { + $doc = new Document([ + '$id' => ID::custom($fileId), + '$permissions' => $permissions, + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getSequence(), + 'name' => $fileName, + 'path' => $path, + 'signature' => '', + 'mimeType' => '', + 'sizeOriginal' => $fileSize, + 'sizeActual' => 0, + 'algorithm' => '', + 'comment' => '', + 'chunksTotal' => $chunks, + 'chunksUploaded' => 0, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => $metadata, + ]); + + try { + $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc); + } catch (DuplicateException) { + throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + } catch (NotFoundException) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + } + } + }, timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'File upload is busy. Try again.'); } - $chunksUploaded = $deviceForFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata); - - if (empty($chunksUploaded)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file'); + if ($completed) { + return; } - if ($chunksUploaded === $chunks) { - if (System::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceForFiles->getType() === Storage::DEVICE_LOCAL) { - $antivirus = new Network( - System::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'), - (int) System::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310) - ); + $finalizeUpload = function (int $chunksUploaded) use ($authorization, $bucket, &$chunks, $contentRange, $dbForProject, $deviceForFiles, $fileId, $fileName, $fileSize, &$metadata, $mergeUploadMetadata, $path, $permissions, $queueForEvents, $response): void { + $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); + $uploaded = 0; - if (!$antivirus->fileScan($path)) { - $deviceForFiles->delete($path); - throw new Exception(Exception::STORAGE_INVALID_FILE); + if (!$file->isEmpty()) { + $chunks = $file->getAttribute('chunksTotal', 1); + $uploaded = $file->getAttribute('chunksUploaded', 0); + $metadata = $mergeUploadMetadata($file->getAttribute('metadata', []), $metadata); + + if ($uploaded === $chunks) { + if (empty($contentRange)) { + throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($file, Response::MODEL_FILE); + + return; } } - $mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption - $fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption - $data = ''; - $iv = ''; - $tag = null; - // Compression - $algorithm = $bucket->getAttribute('compression', Compression::NONE); - if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) { - $data = $deviceForFiles->read($path); - switch ($algorithm) { - case Compression::ZSTD: - $compressor = new Zstd(); - break; - case Compression::GZIP: - default: - $compressor = new GZIP(); - break; - } - $data = $compressor->compress($data); - } else { - // reset the algorithm to none as we do not compress the file - // if file size exceedes the APP_STORAGE_READ_BUFFER - // regardless the bucket compression algoorithm - $algorithm = Compression::NONE; - } + $chunksUploaded = max($uploaded, $chunksUploaded, (int) ($metadata['chunks'] ?? 0)); - if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { - if (empty($data)) { + if ($chunksUploaded === $chunks && $uploaded < $chunks) { + $deviceForFiles->finalizeUpload($path, $chunks, $metadata); + + if (System::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceForFiles->getType() === Storage::DEVICE_LOCAL) { + $antivirus = new Network( + System::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'), + (int) System::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310) + ); + + if (!$antivirus->fileScan($path)) { + $deviceForFiles->delete($path); + throw new Exception(Exception::STORAGE_INVALID_FILE); + } + } + + $mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption + $fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption + $data = ''; + $iv = ''; + $tag = null; + // Compression + $algorithm = $bucket->getAttribute('compression', Compression::NONE); + if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) { $data = $deviceForFiles->read($path); + switch ($algorithm) { + case Compression::ZSTD: + $compressor = new Zstd(); + break; + case Compression::GZIP: + default: + $compressor = new GZIP(); + break; + } + $data = $compressor->compress($data); + } else { + // reset the algorithm to none as we do not compress the file + // if file size exceedes the APP_STORAGE_READ_BUFFER + // regardless the bucket compression algoorithm + $algorithm = Compression::NONE; } - $key = System::getEnv('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); - } - if (!empty($data)) { - if (!$deviceForFiles->write($path, $data, $mimeType)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to save file'); + if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { + if (empty($data)) { + $data = $deviceForFiles->read($path); + } + $key = System::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); } - } - $sizeActual = $deviceForFiles->getFileSize($path); - - $openSSLVersion = null; - $openSSLCipher = null; - $openSSLTag = null; - $openSSLIV = null; - - if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { - $openSSLVersion = '1'; - $openSSLCipher = OpenSSL::CIPHER_AES_128_GCM; - $openSSLTag = \bin2hex($tag); - $openSSLIV = \bin2hex($iv); - } - - if ($file->isEmpty()) { - $doc = new Document([ - '$id' => $fileId, - '$permissions' => $permissions, - 'bucketId' => $bucket->getId(), - 'bucketInternalId' => $bucket->getSequence(), - 'name' => $fileName, - 'path' => $path, - 'signature' => $fileHash, - 'mimeType' => $mimeType, - 'sizeOriginal' => $fileSize, - 'sizeActual' => $sizeActual, - 'algorithm' => $algorithm, - 'comment' => '', - 'chunksTotal' => $chunks, - 'chunksUploaded' => $chunksUploaded, - 'openSSLVersion' => $openSSLVersion, - 'openSSLCipher' => $openSSLCipher, - 'openSSLTag' => $openSSLTag, - 'openSSLIV' => $openSSLIV, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => $metadata, - ]); - - try { - $file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc); - } catch (DuplicateException) { - throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); - } catch (NotFoundException) { - throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + if (!empty($data)) { + if (!$deviceForFiles->write($path, $data, $mimeType)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to save file'); + } } + + $sizeActual = $deviceForFiles->getFileSize($path); + + $openSSLVersion = null; + $openSSLCipher = null; + $openSSLTag = null; + $openSSLIV = null; + + if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { + $openSSLVersion = '1'; + $openSSLCipher = OpenSSL::CIPHER_AES_128_GCM; + $openSSLTag = \bin2hex($tag); + $openSSLIV = \bin2hex($iv); + } + + if ($file->isEmpty()) { + $doc = new Document([ + '$id' => $fileId, + '$permissions' => $permissions, + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getSequence(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $fileHash, + 'mimeType' => $mimeType, + 'sizeOriginal' => $fileSize, + 'sizeActual' => $sizeActual, + 'algorithm' => $algorithm, + 'comment' => '', + 'chunksTotal' => $chunks, + 'chunksUploaded' => $chunksUploaded, + 'openSSLVersion' => $openSSLVersion, + 'openSSLCipher' => $openSSLCipher, + 'openSSLTag' => $openSSLTag, + 'openSSLIV' => $openSSLIV, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => $metadata, + ]); + + try { + $file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc); + } catch (DuplicateException) { + throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); + } catch (NotFoundException) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + } else { + /** + * Skip authorization in updateDocument. + * Without this, the file creation will fail when user doesn't have update permission. + * However as with chunk upload even if we are updating, we are essentially creating a file + * adding it's new chunk so we rely on the create-permission check performed earlier. + */ + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, new Document([ + '$permissions' => $permissions, + 'signature' => $fileHash, + 'mimeType' => $mimeType, + 'sizeActual' => $sizeActual, + 'algorithm' => $algorithm, + 'openSSLVersion' => $openSSLVersion, + 'openSSLCipher' => $openSSLCipher, + 'openSSLTag' => $openSSLTag, + 'openSSLIV' => $openSSLIV, + 'metadata' => $metadata, + 'chunksUploaded' => $chunksUploaded, + ]))); + } + + // Trigger after create success hook + $this->afterCreateSuccess($file); } else { - $file = $file - ->setAttribute('$permissions', $permissions) - ->setAttribute('signature', $fileHash) - ->setAttribute('mimeType', $mimeType) - ->setAttribute('sizeActual', $sizeActual) - ->setAttribute('algorithm', $algorithm) - ->setAttribute('openSSLVersion', $openSSLVersion) - ->setAttribute('openSSLCipher', $openSSLCipher) - ->setAttribute('openSSLTag', $openSSLTag) - ->setAttribute('openSSLIV', $openSSLIV) - ->setAttribute('metadata', $metadata) - ->setAttribute('chunksUploaded', $chunksUploaded); - - /** - * Skip authorization in updateDocument. - * Without this, the file creation will fail when user doesn't have update permission. - * However as with chunk upload even if we are updating, we are essentially creating a file - * adding it's new chunk so we rely on the create-permission check performed earlier. - */ - $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); - } - - // Trigger after create success hook - $this->afterCreateSuccess($file); - } else { - if ($file->isEmpty()) { - $doc = new Document([ - '$id' => ID::custom($fileId), - '$permissions' => $permissions, - 'bucketId' => $bucket->getId(), - 'bucketInternalId' => $bucket->getSequence(), - 'name' => $fileName, - 'path' => $path, - 'signature' => '', - 'mimeType' => '', - 'sizeOriginal' => $fileSize, - 'sizeActual' => 0, - 'algorithm' => '', - 'comment' => '', - 'chunksTotal' => $chunks, - 'chunksUploaded' => $chunksUploaded, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => $metadata, - ]); - - try { - $file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc); - } catch (DuplicateException) { - throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); - } catch (NotFoundException) { - throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); - } - } else { - $file = $file - ->setAttribute('chunksUploaded', $chunksUploaded) - ->setAttribute('metadata', $metadata); - /** * Skip authorization in updateDocument. * Without this, the file creation will fail when user doesn't have update permission. @@ -424,23 +480,41 @@ class Create extends Action * adding it's new chunk so we rely on the create-permission check performed earlier. */ try { - $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); + $file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, new Document([ + 'chunksUploaded' => $chunksUploaded, + 'metadata' => $metadata, + ]))); } catch (NotFoundException) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } } + + if ($chunksUploaded === $chunks) { + $queueForEvents + ->setParam('bucketId', $bucket->getId()) + ->setParam('fileId', $file->getId()) + ->setContext('bucket', $bucket); + } + + $metadata = null; // was causing leaks as it was passed by reference + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($file, Response::MODEL_FILE); + }; + + try { + $chunksUploaded = $deviceForFiles->uploadChunk($fileTmpName, $path, $chunk, $chunks, $metadata); + + if (empty($chunksUploaded)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file'); + } + + $locks($lockKey, 600, fn () => $finalizeUpload($chunksUploaded), timeout: 120.0); + } catch (LockContention) { + $response->addHeader('Retry-After', '5'); + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'File upload is busy. Try again.'); } - - $queueForEvents - ->setParam('bucketId', $bucket->getId()) - ->setParam('fileId', $file->getId()) - ->setContext('bucket', $bucket); - - $metadata = null; // was causing leaks as it was passed by reference - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($file, Response::MODEL_FILE); } /** diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php index 5b44c61d18..6d8781d484 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Storage\Http\Buckets\Files; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -64,7 +65,7 @@ class Delete extends Action ->inject('dbForProject') ->inject('queueForEvents') ->inject('deviceForFiles') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('authorization') ->inject('user') ->callback($this->action(...)); @@ -77,7 +78,7 @@ class Delete extends Action Database $dbForProject, Event $queueForEvents, Device $deviceForFiles, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, Authorization $authorization, User $user, ) { @@ -126,11 +127,12 @@ class Delete extends Action } if ($deviceDeleted) { - $queueForDeletes - ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) - ->setResourceType('bucket/' . $bucket->getId()) - ->setResource('file/' . $fileId) - ; + $publisherForDeletes->enqueue(new DeleteMessage( + project: $queueForEvents->getProject(), + type: DELETE_TYPE_CACHE_BY_RESOURCE, + resource: 'file/' . $fileId, + resourceType: 'bucket/' . $bucket->getId(), + )); try { if ($fileSecurity && !$valid) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index cb511d5231..68bc2cabae 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -131,7 +131,6 @@ class Get extends Action throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); } - /* @type Document $bucket */ $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = $user->isApp($authorization->getRoles()); @@ -155,7 +154,6 @@ class Get extends Action if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { - /* @type Document $file */ $file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php index 0cb7c54a26..3bae031e06 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Modules\Teams\Http\Teams; -use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\Platform\Workers\Deletes; @@ -55,13 +56,13 @@ class Delete extends Action ->inject('response') ->inject('getProjectDB') ->inject('dbForProject') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('queueForEvents') ->inject('project') ->callback($this->action(...)); } - public function action(string $teamId, Response $response, callable $getProjectDB, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Document $project) + public function action(string $teamId, Response $response, callable $getProjectDB, Database $dbForProject, DeletePublisher $publisherForDeletes, Event $queueForEvents, Document $project) { $team = $dbForProject->getDocument('teams', $teamId); @@ -79,15 +80,18 @@ class Delete extends Action // Async delete if ($project->getId() === 'console') { - $queueForDeletes - ->setType(DELETE_TYPE_TEAM_PROJECTS) - ->setDocument($team) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_TEAM_PROJECTS, + document: $team, + )); } - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($team); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $team, + )); $queueForEvents ->setParam('teamId', $team->getId()) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 8bc090bb03..a6f0e7fd6d 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -21,6 +21,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Span\Span; use Utopia\System\System; +use Utopia\Validator\Contains; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -59,9 +60,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; @@ -95,6 +96,13 @@ trait Deployment $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resourceInternalId = $resource->getSequence(); + $validator = new Contains(VCS_DEPLOYMENT_SKIP_PATTERNS); + if ($validator->isValid($providerCommitMessage)) { + Span::add("{$logBase}.build.skipped.reason", $validator->getDescription()); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); @@ -561,4 +569,5 @@ trait Deployment { return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); } + } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php index 26a9476941..5d90d6d231 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php @@ -2,7 +2,8 @@ namespace Appwrite\Platform\Modules\VCS\Http\Installations; -use Appwrite\Event\Delete as DeleteEvent; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; @@ -11,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Text; @@ -49,7 +51,8 @@ class Delete extends Action ->param('installationId', '', new Text(256), 'Installation Id') ->inject('response') ->inject('dbForPlatform') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') + ->inject('project') ->callback($this->action(...)); } @@ -57,7 +60,8 @@ class Delete extends Action string $installationId, Response $response, Database $dbForPlatform, - DeleteEvent $queueForDeletes + DeletePublisher $publisherForDeletes, + Document $project, ) { $installation = $dbForPlatform->getDocument('installations', $installationId); @@ -69,9 +73,11 @@ class Delete extends Action throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB'); } - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($installation); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $installation, + )); $response->noContent(); } 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/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index fe803f1292..e43281545a 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Event\Delete; +use Appwrite\Event\Message\Delete as DeleteMessage; use Appwrite\Event\Publisher\Certificate; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use DateInterval; use DateTime; use Utopia\Console; @@ -30,11 +31,11 @@ class Maintenance extends Action ->inject('dbForPlatform') ->inject('console') ->inject('publisherForCertificates') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->callback($this->action(...)); } - public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void + public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, DeletePublisher $publisherForDeletes): void { Console::title('Maintenance V1'); Console::success(APP_NAME . ' maintenance process v1 has started'); @@ -59,7 +60,7 @@ class Maintenance extends Action $delay = $next->getTimestamp() - $now->getTimestamp(); } - $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) { + $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $publisherForDeletes, $publisherForCertificates) { $time = DatabaseDateTime::now(); Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); @@ -70,12 +71,12 @@ class Maintenance extends Action $dbForPlatform->foreach( 'projects', - function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) { - $queueForDeletes - ->setType(DELETE_TYPE_MAINTENANCE) - ->setProject($project) - ->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly)) - ->trigger(); + function (Document $project) use ($publisherForDeletes, $usageStatsRetentionHourly) { + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_MAINTENANCE, + hourlyUsageRetentionDatetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly), + )); }, [ Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), @@ -85,17 +86,17 @@ class Maintenance extends Action ] ); - $queueForDeletes - ->setType(DELETE_TYPE_MAINTENANCE) - ->setProject($console) - ->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly)) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + project: $console, + type: DELETE_TYPE_MAINTENANCE, + hourlyUsageRetentionDatetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly), + )); - $this->notifyDeleteConnections($queueForDeletes); + $this->notifyDeleteConnections($publisherForDeletes); $this->renewCertificates($dbForPlatform, $publisherForCertificates); - $this->notifyDeleteCache($cacheRetention, $queueForDeletes); - $this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes); - $this->notifyDeleteCSVExports($queueForDeletes); + $this->notifyDeleteCache($cacheRetention, $publisherForDeletes); + $this->notifyDeleteSchedules($schedulesDeletionRetention, $publisherForDeletes); + $this->notifyDeleteCSVExports($publisherForDeletes); }; if ($type === 'loop') { @@ -109,19 +110,17 @@ class Maintenance extends Action } } - private function notifyDeleteConnections(Delete $queueForDeletes): void + private function notifyDeleteConnections(DeletePublisher $publisherForDeletes): void { - $queueForDeletes - ->setType(DELETE_TYPE_REALTIME) - ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -60)) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + type: DELETE_TYPE_REALTIME, + datetime: DatabaseDateTime::addSeconds(new \DateTime(), -60), + )); } - private function notifyDeleteCSVExports(Delete $queueForDeletes): void + private function notifyDeleteCSVExports(DeletePublisher $publisherForDeletes): void { - $queueForDeletes - ->setType(DELETE_TYPE_CSV_EXPORTS) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage(type: DELETE_TYPE_CSV_EXPORTS)); } private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void @@ -172,19 +171,19 @@ class Maintenance extends Action } } - private function notifyDeleteCache($interval, Delete $queueForDeletes): void + private function notifyDeleteCache($interval, DeletePublisher $publisherForDeletes): void { - $queueForDeletes - ->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP) - ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval)) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + type: DELETE_TYPE_CACHE_BY_TIMESTAMP, + datetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval), + )); } - private function notifyDeleteSchedules($interval, Delete $queueForDeletes): void + private function notifyDeleteSchedules($interval, DeletePublisher $publisherForDeletes): void { - $queueForDeletes - ->setType(DELETE_TYPE_SCHEDULES) - ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval)) - ->trigger(); + $publisherForDeletes->enqueue(new DeleteMessage( + type: DELETE_TYPE_SCHEDULES, + datetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval), + )); } } 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..a58fc48098 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -5,7 +5,8 @@ namespace Appwrite\Platform\Workers; use Appwrite\Certificates\Adapter as CertificatesAdapter; use Appwrite\Deletes\Identities; use Appwrite\Deletes\Targets; -use Appwrite\Event\Delete as DeleteEvent; +use Appwrite\Event\Message\Delete as DeleteMessage; +use Appwrite\Event\Publisher\Delete as DeletePublisher; use Appwrite\Extend\Exception; use Executor\Executor; use Throwable; @@ -66,7 +67,7 @@ class Deletes extends Action ->inject('executionsRetentionCount') ->inject('auditRetention') ->inject('log') - ->inject('queueForDeletes') + ->inject('publisherForDeletes') ->inject('getAudit') ->callback($this->action(...)); } @@ -93,7 +94,7 @@ class Deletes extends Action int $executionsRetentionCount, string $auditRetention, Log $log, - DeleteEvent $queueForDeletes, + DeletePublisher $publisherForDeletes, callable $getAudit, ): void { $payload = $message->getPayload(); @@ -102,12 +103,13 @@ class Deletes extends Action throw new Exception('Missing payload'); } - $type = $payload['type'] ?? ''; - $datetime = $payload['datetime'] ?? null; - $hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null; - $resource = $payload['resource'] ?? null; - $resourceType = $payload['resourceType'] ?? null; - $document = new Document($payload['document'] ?? []); + $deleteMessage = DeleteMessage::fromArray($payload); + $type = $deleteMessage->type; + $datetime = $deleteMessage->datetime; + $hourlyUsageRetentionDatetime = $deleteMessage->hourlyUsageRetentionDatetime; + $resource = $deleteMessage->resource; + $resourceType = $deleteMessage->resourceType; + $document = $deleteMessage->document ?? new Document(); $log->addTag('projectId', $project->getId()); $log->addTag('type', $type); @@ -142,7 +144,7 @@ class Deletes extends Action case DELETE_TYPE_RULES: $this->deleteRule($dbForPlatform, $document, $certificates); break; - case DELETE_TYPE_TRANSACTION: + case DELETE_TYPE_TRANSACTIONS: $this->deleteTransactionLogs($getProjectDB, $document, $project); break; default: @@ -214,13 +216,27 @@ class Deletes extends Action $this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime); $this->deleteExpiredSessions($project, $getProjectDB); $this->deleteExpiredTransactions($project, $getProjectDB); - $this->deleteOldDeployments($queueForDeletes, $project, $getProjectDB); + $this->deleteOldDeployments($publisherForDeletes, $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, @@ -376,12 +392,12 @@ class Deletes extends Action Targets::delete($getProjectDB($project), Query::equal('sessionInternalId', [$session->getSequence()])); } - private function deleteOldDeployments(DeleteEvent $queueForDeletes, Document $project, callable $getProjectDB): void + private function deleteOldDeployments(DeletePublisher $publisherForDeletes, Document $project, callable $getProjectDB): void { /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); - $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes, $project) { + $removalCallback = function (Document $resource) use ($dbForProject, $publisherForDeletes, $project) { $retention = $resource->getAttribute('deploymentRetention', 0); // 0 means unlimited - never delete @@ -406,12 +422,12 @@ class Deletes extends Action 'deployments', $queries, $dbForProject, - function (Document $deployment) use ($queueForDeletes, $project) { - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($deployment) - ->setProject($project) - ->trigger(); + function (Document $deployment) use ($publisherForDeletes, $project) { + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $deployment, + )); } ); }; @@ -716,6 +732,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 649fab5233..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); } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index d1da91a496..2dd59c7b4c 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -293,6 +293,8 @@ class Migrations extends Action $this->dbForProject, $this->getDatabasesDB, Config::getParam('collections', [])['databases']['collections'], + $this->dbForPlatform, + $this->project->getSequence(), OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail, $this->resolveDestinationDatabaseDsn(...), ), diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php index fc67dedb13..6c5d50e016 100644 --- a/src/Appwrite/SDK/Specification/Format.php +++ b/src/Appwrite/SDK/Specification/Format.php @@ -466,6 +466,14 @@ abstract class Format return 'ConsoleResourceValue'; } break; + case 'getEmailTemplate': + switch ($param) { + case 'templateId': + return 'ProjectEmailTemplateId'; + case 'locale': + return 'ProjectEmailTemplateLocale'; + } + break; } break; case 'account': diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 3be3fe7115..68ab7a0986 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -4,6 +4,7 @@ namespace Appwrite\SDK\Specification\Format; use Appwrite\Platform\Tasks\Specs; use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\MethodType; use Appwrite\SDK\Response; @@ -291,6 +292,21 @@ class OpenAPI3 extends Format } if (!(\is_array($model)) && $model->isNone()) { + if ($produces === ContentType::TEXT->value && !\in_array($response->getCode(), [204, 301, 302, 308], true)) { + $temp['responses'][(string)$response->getCode()] = [ + 'description' => 'Text', + 'content' => [ + $produces => [ + 'schema' => [ + 'type' => 'string', + ], + ], + ], + ]; + + continue; + } + $temp['responses'][(string)$response->getCode()] = [ 'description' => in_array($produces, [ 'image/*', diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 9a16bc8bbe..fb1ef66eca 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -4,6 +4,7 @@ namespace Appwrite\SDK\Specification\Format; use Appwrite\Platform\Tasks\Specs; use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\MethodType; use Appwrite\SDK\Response; @@ -298,6 +299,17 @@ class Swagger2 extends Format } if (!(\is_array($model)) && $model->isNone()) { + if ($produces === ContentType::TEXT->value && !\in_array($response->getCode(), [204, 301, 302, 308], true)) { + $temp['responses'][(string)$response->getCode()] = [ + 'description' => 'Text', + 'schema' => [ + 'type' => 'string', + ], + ]; + + continue; + } + $temp['responses'][(string)$response->getCode()] = [ 'description' => in_array($produces, [ 'image/*', 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 @@ +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/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index 850e4b5ae9..388630af3f 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -53,6 +53,12 @@ class MigrationReport extends Model 'default' => 0, 'example' => 20, ]) + ->addRule(Resource::TYPE_PLATFORM, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of platforms to be migrated.', + 'default' => 0, + 'example' => 5, + ]) ->addRule(Resource::TYPE_SITE, [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of sites to be migrated.', 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/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index c8f921f2ec..43daba470b 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -175,4 +175,49 @@ class ConsoleConsoleClientTest extends Scope $this->assertNotNull($usersRead); $this->assertEquals('Access to read users', $usersRead['description']); } + + public function testListOrganizationScopes(): void + { + $response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertIsArray($response['body']['scopes']); + $this->assertGreaterThan(0, $response['body']['total']); + $this->assertEquals($response['body']['total'], \count($response['body']['scopes'])); + + $scopeIds = \array_column($response['body']['scopes'], '$id'); + + // Well-known scopes must be present + $this->assertContains('projects.read', $scopeIds); + $this->assertContains('projects.write', $scopeIds); + + // Every scope has the expected shape + foreach ($response['body']['scopes'] as $scope) { + $this->assertArrayHasKey('$id', $scope); + $this->assertIsString($scope['$id']); + $this->assertNotEmpty($scope['$id']); + $this->assertArrayHasKey('description', $scope); + $this->assertIsString($scope['description']); + $this->assertNotEmpty($scope['description']); + $this->assertArrayHasKey('deprecated', $scope); + $this->assertIsBool($scope['deprecated']); + $this->assertArrayHasKey('category', $scope); + $this->assertIsString($scope['category']); + } + + // A specific scope has the expected description + $projectsRead = null; + foreach ($response['body']['scopes'] as $scope) { + if ($scope['$id'] === 'projects.read') { + $projectsRead = $scope; + break; + } + } + $this->assertNotNull($projectsRead); + $this->assertEquals('Access to read organization projects', $projectsRead['description']); + } } diff --git a/tests/e2e/Services/Console/ConsoleCustomServerTest.php b/tests/e2e/Services/Console/ConsoleCustomServerTest.php index f06011843f..e7a95fd357 100644 --- a/tests/e2e/Services/Console/ConsoleCustomServerTest.php +++ b/tests/e2e/Services/Console/ConsoleCustomServerTest.php @@ -74,4 +74,35 @@ class ConsoleCustomServerTest extends Scope $this->assertArrayHasKey('deprecated', $usersRead); $this->assertIsBool($usersRead['deprecated']); } + + public function testListOrganizationScopes(): void + { + // Public endpoint: must succeed without admin authentication. Drop the + // headers from getHeaders() and only pass project + content-type. + $response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertIsArray($response['body']['scopes']); + $this->assertGreaterThan(0, $response['body']['total']); + + $scopeIds = \array_column($response['body']['scopes'], '$id'); + $this->assertContains('projects.read', $scopeIds); + + $projectsRead = null; + foreach ($response['body']['scopes'] as $scope) { + if ($scope['$id'] === 'projects.read') { + $projectsRead = $scope; + break; + } + } + $this->assertNotNull($projectsRead); + $this->assertIsString($projectsRead['description']); + $this->assertNotEmpty($projectsRead['description']); + $this->assertArrayHasKey('deprecated', $projectsRead); + $this->assertIsBool($projectsRead['deprecated']); + } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f08b711fb2..b1f07c3f9d 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1199,6 +1199,144 @@ class FunctionsCustomServerTest extends Scope }, 120000, 500); } + public function testCreateDeploymentParallelChunksLargeFile(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Parallel Chunk Deployment', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + + $deploymentId = ID::unique(); + $tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-function-deployment-' . $deploymentId; + + mkdir($tmpDirectory); + + try { + copy(__DIR__ . '/../../../resources/functions/basic/index.js', $tmpDirectory . DIRECTORY_SEPARATOR . 'index.js'); + file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'large.bin', random_bytes(20 * 1024 * 1024)); + + $source = $tmpDirectory . DIRECTORY_SEPARATOR . 'code.tar.gz'; + Console::execute('cd ' . $tmpDirectory . ' && tar --exclude code.tar.gz -czf code.tar.gz .', '', $this->stdout, $this->stderr); + + $totalSize = filesize($source); + $chunkSize = 5 * 1024 * 1024; + $chunksTotal = (int) ceil($totalSize / $chunkSize); + + $this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test deployment must span at least 4 chunks'); + + $requests = []; + $sourceHandle = fopen($source, 'rb'); + $this->assertNotFalse($sourceHandle, 'Could not open deployment package'); + + try { + for ($i = 0; $i < $chunksTotal; $i++) { + $start = $i * $chunkSize; + $end = min($start + $chunkSize, $totalSize) - 1; + $length = $end - $start + 1; + $chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part'; + + fseek($sourceHandle, $start); + file_put_contents($chunkPath, fread($sourceHandle, $length)); + + $requests[] = [ + 'headers' => [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-appwrite-id' => $deploymentId, + 'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize, + ], + 'chunkPath' => $chunkPath, + ]; + } + } finally { + fclose($sourceHandle); + } + + $responses = []; + $endpoint = parse_url($this->client->getEndpoint()); + $scheme = $endpoint['scheme'] ?? 'http'; + $host = $endpoint['host'] ?? 'appwrite'; + $port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80); + $basePath = rtrim($endpoint['path'] ?? '', '/'); + + \Swoole\Coroutine\run(function () use ($basePath, $functionId, $host, $port, $requests, $scheme, &$responses): void { + $wg = new \Swoole\Coroutine\WaitGroup(); + + foreach ($requests as $index => $request) { + $wg->add(); + \Swoole\Coroutine::create(function () use ($basePath, $functionId, $host, $index, $port, $request, &$responses, $scheme, $wg): void { + try { + for ($attempt = 0; $attempt < 3; $attempt++) { + $client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https'); + $client->set([ + 'timeout' => 300, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false, + ]); + $client->setHeaders($request['headers']); + $client->setMethod(Client::METHOD_POST); + $client->setData([ + 'entrypoint' => 'index.js', + 'activate' => true, + ]); + $client->addFile($request['chunkPath'], 'code', 'application/x-gzip', 'code.tar.gz'); + $client->execute($basePath . '/functions/' . $functionId . '/deployments'); + + $responses[$index] = [ + 'body' => $client->body, + 'error' => $client->errMsg, + 'headers' => $client->headers ?? [], + 'statusCode' => $client->statusCode, + ]; + + $client->close(); + + if ($responses[$index]['statusCode'] !== 429) { + break; + } + + $retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1); + \Swoole\Coroutine::sleep(max($retryAfter, 0.1)); + } + } finally { + $wg->done(); + } + }); + } + + $wg->wait(); + }); + + ksort($responses); + + foreach ($responses as $response) { + $this->assertSame('', $response['error']); + $this->assertContains($response['statusCode'], [202], (string) $response['body']); + } + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('ready', $deployment['body']['status']); + $this->assertEquals($deploymentId, $deployment['body']['$id']); + }, 120000, 500); + } finally { + $this->cleanupFunction($functionId); + + if (is_dir($tmpDirectory)) { + foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) { + unlink($file); + } + rmdir($tmpDirectory); + } + } + } + public function testUpdateDeployment(): void { $data = $this->setupTestDeployment(); diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 8dd5b2fef6..82fcee4838 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -2463,6 +2463,83 @@ trait MigrationsBase return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); } + /** + * Integrations + */ + public function testAppwriteMigrationPlatform(): void + { + $sourceHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]; + + $destinationHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]; + + // Create platform on source project + $response = $this->client->call(Client::METHOD_POST, '/project/platforms/web', $sourceHeaders, [ + 'platformId' => ID::unique(), + 'name' => 'Test Platform', + 'hostname' => 'localhost', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + $platform = $response['body']; + + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_PLATFORM, + ], + 'endpoint' => $this->webEndpoint, + 'projectId' => $this->getProject()['$id'], + 'apiKey' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals('completed', $result['status']); + $this->assertEquals([Resource::TYPE_PLATFORM], $result['resources']); + $this->assertArrayHasKey(Resource::TYPE_PLATFORM, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['error']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['pending']); + $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_PLATFORM]['success']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['processing']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['warning']); + + // Verify platform on destination project using the project's API key + $response = $this->client->call(Client::METHOD_GET, '/project/platforms', $destinationHeaders); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, $response['body']['total']); + + $foundPlatform = null; + + foreach ($response['body']['platforms'] as $p) { + if ($p['name'] === 'Test Platform' && $p['type'] === 'web') { + $foundPlatform = $p; + + break; + } + } + + $this->assertNotNull($foundPlatform); + $this->assertEquals('web', $foundPlatform['type']); + $this->assertEquals('Test Platform', $foundPlatform['name']); + $this->assertEquals('localhost', $foundPlatform['hostname']); + + // Cleanup on destination + $this->client->call(Client::METHOD_DELETE, '/project/platforms/' . $foundPlatform['$id'], $destinationHeaders); + + // Cleanup on source + $this->client->call(Client::METHOD_DELETE, '/project/platforms/' . $platform['$id'], $sourceHeaders); + } + /** * Import documents from a CSV file. */ diff --git a/tests/e2e/Services/Project/TemplatesBase.php b/tests/e2e/Services/Project/TemplatesBase.php index b240c945b3..11dc6dc80b 100644 --- a/tests/e2e/Services/Project/TemplatesBase.php +++ b/tests/e2e/Services/Project/TemplatesBase.php @@ -1147,6 +1147,115 @@ trait TemplatesBase return $this->client->call(Client::METHOD_PATCH, '/project/templates/email', $headers, $params); } + // Console email template (default) tests + + public function testGetConsoleEmailTemplate(): void + { + $response = $this->getConsoleEmailTemplate('verification', 'en'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('verification', $response['body']['templateId']); + $this->assertSame('en', $response['body']['locale']); + $this->assertNotEmpty($response['body']['subject']); + $this->assertNotEmpty($response['body']['message']); + $this->assertSame('', $response['body']['senderName']); + $this->assertSame('', $response['body']['senderEmail']); + $this->assertSame('', $response['body']['replyToEmail']); + $this->assertSame('', $response['body']['replyToName']); + } + + public function testGetConsoleEmailTemplateIgnoresCustomOverride(): void + { + $this->ensureSMTPEnabled(); + + // Set a custom override on the project template. + $this->updateEmailTemplate( + templateId: 'recovery', + locale: 'en', + subject: 'Custom subject', + message: 'Custom message', + senderName: 'Custom Sender', + senderEmail: 'custom@appwrite.io', + ); + + // Console endpoint must always return the built-in default, not the override. + $response = $this->getConsoleEmailTemplate('recovery', 'en'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('recovery', $response['body']['templateId']); + $this->assertNotSame('Custom subject', $response['body']['subject']); + $this->assertSame('', $response['body']['senderName']); + $this->assertSame('', $response['body']['senderEmail']); + } + + public function testGetConsoleEmailTemplateDefaultLocale(): void + { + $response = $this->getConsoleEmailTemplate('magicSession'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('en', $response['body']['locale']); + $this->assertNotEmpty($response['body']['subject']); + } + + public function testGetConsoleEmailTemplateNonDefaultLocale(): void + { + $response = $this->getConsoleEmailTemplate('verification', 'fr'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('verification', $response['body']['templateId']); + $this->assertSame('fr', $response['body']['locale']); + $this->assertNotEmpty($response['body']['subject']); + $this->assertNotEmpty($response['body']['message']); + } + + public function testGetConsoleEmailTemplateAllTypes(): void + { + $types = [ + 'verification', + 'magicSession', + 'recovery', + 'invitation', + 'mfaChallenge', + 'sessionAlert', + 'otpSession', + ]; + + foreach ($types as $type) { + $response = $this->getConsoleEmailTemplate($type, 'en'); + $this->assertSame(200, $response['headers']['status-code'], "type={$type}"); + $this->assertNotEmpty($response['body']['subject'], "type={$type} must have subject"); + $this->assertNotEmpty($response['body']['message'], "type={$type} must have message"); + } + } + + public function testGetConsoleEmailTemplateInvalidTemplateId(): void + { + $response = $this->getConsoleEmailTemplate('invalidTemplate', 'en'); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testGetConsoleEmailTemplateInvalidLocale(): void + { + $response = $this->getConsoleEmailTemplate('recovery', 'not-a-locale'); + + $this->assertSame(400, $response['headers']['status-code']); + } + + protected function getConsoleEmailTemplate(string $templateId, ?string $locale = null): mixed + { + $params = []; + if ($locale !== null) { + $params['locale'] = $locale; + } + + return $this->client->call(Client::METHOD_GET, '/console/templates/email/' . $templateId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ], $params); + } + protected function ensureSMTPEnabled(): void { $this->client->call( diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 48de610365..c83958afe1 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -70,6 +70,53 @@ trait ProxyBase $this->assertEquals(204, $rule['headers']['status-code']); } + public function testCreateRuleDeletesOrphanedRule(): void + { + $domain = \uniqid() . '-orphan-api.custom.localhost'; + $orphanProject = $this->getProject(true); + + $orphanRule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $orphanProject['$id'], + 'x-appwrite-key' => $orphanProject['apiKey'], + ], [ + 'domain' => $domain, + ]); + + $this->assertEquals(201, $orphanRule['headers']['status-code']); + $this->assertEquals($domain, $orphanRule['body']['domain']); + + $duplicateRule = $this->createAPIRule($domain); + $this->assertEquals(409, $duplicateRule['headers']['status-code']); + + $deleteProject = $this->client->call(Client::METHOD_DELETE, '/projects/' . $orphanProject['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $orphanProject['$id'], + 'x-appwrite-key' => $orphanProject['apiKey'], + ]); + + $this->assertEquals(204, $deleteProject['headers']['status-code']); + + // Project deletion removes the project document synchronously, while rule cleanup is queued. + // Creating the same domain now should clean up that orphaned rule before retrying. + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('domain', [$domain])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertEquals($rule['body']['$id'], $rules['body']['rules'][0]['$id']); + + $this->cleanupRule($rule['body']['$id']); + } + public function testCreateRuleSetup(): void { $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); @@ -171,8 +218,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 +234,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,7 +256,8 @@ trait ProxyBase $this->assertEquals(200, $rules['headers']['status-code']); $this->assertEquals(2, $rules['body']['total']); - $this->cleanupRule($ruleId); + $this->cleanupRule($ruleId301); + $this->cleanupRule($ruleId307); $this->cleanupSite($siteId); } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index a32b990b9e..9cca689780 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1351,6 +1351,145 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + public function testCreateDeploymentParallelChunksLargeFile(): void + { + $siteId = $this->setupSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => '', + 'framework' => 'other', + 'name' => 'Test Site Parallel Chunk Deployment', + 'outputDirectory' => './', + 'providerBranch' => 'main', + 'providerRootDirectory' => './', + 'siteId' => ID::unique() + ]); + + $deploymentId = ID::unique(); + $tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-site-deployment-' . $deploymentId; + + mkdir($tmpDirectory); + + try { + file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'index.html', 'Hello World'); + file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'large.bin', random_bytes(20 * 1024 * 1024)); + + $source = $tmpDirectory . DIRECTORY_SEPARATOR . 'code.tar.gz'; + Console::execute('cd ' . $tmpDirectory . ' && tar --exclude code.tar.gz -czf code.tar.gz .', '', $this->stdout, $this->stderr); + + $totalSize = filesize($source); + $chunkSize = 5 * 1024 * 1024; + $chunksTotal = (int) ceil($totalSize / $chunkSize); + + $this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test deployment must span at least 4 chunks'); + + $requests = []; + $sourceHandle = fopen($source, 'rb'); + $this->assertNotFalse($sourceHandle, 'Could not open deployment package'); + + try { + for ($i = 0; $i < $chunksTotal; $i++) { + $start = $i * $chunkSize; + $end = min($start + $chunkSize, $totalSize) - 1; + $length = $end - $start + 1; + $chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part'; + + fseek($sourceHandle, $start); + file_put_contents($chunkPath, fread($sourceHandle, $length)); + + $requests[] = [ + 'headers' => [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-appwrite-id' => $deploymentId, + 'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize, + ], + 'chunkPath' => $chunkPath, + ]; + } + } finally { + fclose($sourceHandle); + } + + $responses = []; + $endpoint = parse_url($this->client->getEndpoint()); + $scheme = $endpoint['scheme'] ?? 'http'; + $host = $endpoint['host'] ?? 'appwrite'; + $port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80); + $basePath = rtrim($endpoint['path'] ?? '', '/'); + + \Swoole\Coroutine\run(function () use ($basePath, $host, $port, $requests, $scheme, $siteId, &$responses): void { + $wg = new \Swoole\Coroutine\WaitGroup(); + + foreach ($requests as $index => $request) { + $wg->add(); + \Swoole\Coroutine::create(function () use ($basePath, $host, $index, $port, $request, &$responses, $scheme, $siteId, $wg): void { + try { + for ($attempt = 0; $attempt < 3; $attempt++) { + $client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https'); + $client->set([ + 'timeout' => 300, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false, + ]); + $client->setHeaders($request['headers']); + $client->setMethod(Client::METHOD_POST); + $client->setData([ + 'activate' => true, + ]); + $client->addFile($request['chunkPath'], 'code', 'application/x-gzip', 'code.tar.gz'); + $client->execute($basePath . '/sites/' . $siteId . '/deployments'); + + $responses[$index] = [ + 'body' => $client->body, + 'error' => $client->errMsg, + 'headers' => $client->headers ?? [], + 'statusCode' => $client->statusCode, + ]; + + $client->close(); + + if ($responses[$index]['statusCode'] !== 429) { + break; + } + + $retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1); + \Swoole\Coroutine::sleep(max($retryAfter, 0.1)); + } + } finally { + $wg->done(); + } + }); + } + + $wg->wait(); + }); + + ksort($responses); + + foreach ($responses as $response) { + $this->assertSame('', $response['error']); + $this->assertContains($response['statusCode'], [202], (string) $response['body']); + } + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('ready', $deployment['body']['status']); + $this->assertEquals($deploymentId, $deployment['body']['$id']); + }, 120000, 500); + } finally { + $this->cleanupSite($siteId); + + if (is_dir($tmpDirectory)) { + foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) { + unlink($file); + } + rmdir($tmpDirectory); + } + } + } + public function testCreateDeployment() { $siteId = $this->setupSite([ diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 5e09031a9c..375e526fcf 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -391,7 +391,7 @@ trait StorageBase 'bucketId' => ID::unique(), 'name' => 'Test Bucket 2', 'fileSecurity' => true, - 'maximumFileSize' => 6000000000, //6GB + 'maximumFileSize' => 6000000001, 'allowedFileExtensions' => ["jpg", "png"], 'permissions' => [ Permission::read(Role::any()), @@ -1436,6 +1436,184 @@ trait StorageBase ]); } + public function testCreateBucketFileParallelChunksLargeFile(): void + { + $totalSize = 20 * 1024 * 1024; + $chunkSize = 5 * 1024 * 1024; + $chunksTotal = (int) ceil($totalSize / $chunkSize); + + $this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test file must span at least 4 chunks'); + + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket Parallel Chunk Upload', + 'fileSecurity' => true, + 'maximumFileSize' => $totalSize, + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $bucket['headers']['status-code']); + + $bucketId = $bucket['body']['$id']; + $fileId = ID::unique(); + $tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-upload-' . $fileId; + $source = $tmpDirectory . DIRECTORY_SEPARATOR . 'large-parallel-upload.bin'; + + mkdir($tmpDirectory); + + try { + $handle = fopen($source, 'wb'); + $this->assertNotFalse($handle, 'Could not create test file'); + + $remaining = $totalSize; + $block = str_repeat(hash('sha256', $fileId, binary: true), 1024); + while ($remaining > 0) { + $bytes = substr($block, 0, min(strlen($block), $remaining)); + fwrite($handle, $bytes); + $remaining -= strlen($bytes); + } + fclose($handle); + + $requests = []; + + $sourceHandle = fopen($source, 'rb'); + $this->assertNotFalse($sourceHandle, 'Could not open test file'); + + for ($i = 0; $i < $chunksTotal; $i++) { + $start = $i * $chunkSize; + $end = min($start + $chunkSize, $totalSize) - 1; + $length = $end - $start + 1; + $chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part'; + + fseek($sourceHandle, $start); + file_put_contents($chunkPath, fread($sourceHandle, $length)); + + $requests[] = [ + 'headers' => [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + 'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize, + ], + 'chunkPath' => $chunkPath, + ]; + } + fclose($sourceHandle); + + $responses = []; + $endpoint = parse_url($this->client->getEndpoint()); + $scheme = $endpoint['scheme'] ?? 'http'; + $host = $endpoint['host'] ?? 'appwrite'; + $port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80); + $basePath = rtrim($endpoint['path'] ?? '', '/'); + + \Swoole\Coroutine\run(function () use ($basePath, $bucketId, $fileId, $host, $port, $requests, $scheme, &$responses): void { + $wg = new \Swoole\Coroutine\WaitGroup(); + + foreach ($requests as $index => $request) { + $wg->add(); + \Swoole\Coroutine::create(function () use ($basePath, $bucketId, $fileId, $host, $index, $port, $request, &$responses, $scheme, $wg): void { + try { + for ($attempt = 0; $attempt < 3; $attempt++) { + $client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https'); + $client->set([ + 'timeout' => 300, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false, + ]); + $client->setHeaders($request['headers']); + $client->setMethod(Client::METHOD_POST); + $client->setData([ + 'fileId' => $fileId, + 'permissions[0]' => Permission::read(Role::any()), + 'permissions[1]' => Permission::delete(Role::any()), + ]); + $client->addFile($request['chunkPath'], 'file', 'application/octet-stream', 'large-parallel-upload.bin'); + $client->execute($basePath . '/storage/buckets/' . $bucketId . '/files'); + + $responses[$index] = [ + 'body' => $client->body, + 'error' => $client->errMsg, + 'headers' => $client->headers ?? [], + 'statusCode' => $client->statusCode, + ]; + + $client->close(); + + if ($responses[$index]['statusCode'] !== 429) { + break; + } + + $retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1); + \Swoole\Coroutine::sleep(max($retryAfter, 0.1)); + } + } finally { + $wg->done(); + } + }); + } + + $wg->wait(); + }); + + ksort($responses); + + foreach ($responses as $response) { + $this->assertSame('', $response['error']); + $this->assertContains($response['statusCode'], [200, 201], (string) $response['body']); + } + + $uploadedFile = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(200, $uploadedFile['headers']['status-code']); + $this->assertEquals($chunksTotal, $uploadedFile['body']['chunksTotal']); + $this->assertEquals($chunksTotal, $uploadedFile['body']['chunksUploaded']); + + $download = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(200, $download['headers']['status-code']); + $this->assertEquals($totalSize, strlen($download['body'])); + $this->assertEquals(hash_file('sha256', $source), hash('sha256', $download['body'])); + } finally { + if (isset($bucketId)) { + $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + } + + foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) { + unlink($file); + } + + if (is_dir($tmpDirectory)) { + rmdir($tmpDirectory); + } + } + } + public function testDeleteBucketFile(): void { // Create a fresh file just for deletion testing (not using cache since we delete it) 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)); + } +}