diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 494a9c1424..d8256ddc7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -512,7 +512,7 @@ jobs: # Services that rely on sequential test method execution (shared static state) FUNCTIONAL_FLAG="--functional" case "${{ matrix.service }}" in - Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;; + Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;; esac docker compose exec -T \ diff --git a/app/cli.php b/app/cli.php index 73908510d9..a6267fa341 100644 --- a/app/cli.php +++ b/app/cli.php @@ -2,10 +2,10 @@ require_once __DIR__ . '/init.php'; -use Appwrite\Event\Certificate; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Func; +use Appwrite\Event\Publisher\Certificate as CertificatePublisher; use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Platform\Appwrite; @@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) ), ['publisher']); +$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher( + $publisher, + new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME)) +), ['publisher']); $container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher( $publisher, new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME)) @@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) { $container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); -$container->set('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); -}, ['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/controllers/general.php b/app/controllers/general.php index 542effc091..b4f4a5c1d1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException; use Appwrite\Auth\Key; use Appwrite\Bus\Events\ExecutionCompleted; use Appwrite\Bus\Events\RequestCompleted; -use Appwrite\Event\Certificate; use Appwrite\Event\Delete as DeleteEvent; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Network\Cors; use Appwrite\Platform\Appwrite; @@ -1014,11 +1014,11 @@ Http::init() ->inject('request') ->inject('console') ->inject('dbForPlatform') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('platform') ->inject('authorization') ->inject('certifiedDomains') - ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { + ->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) { $hostname = $request->getHostname(); $platformHostnames = $platform['hostnames'] ?? []; @@ -1044,7 +1044,7 @@ Http::init() } // 4. Check/create rule (requires DB access) - $authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) { + $authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) { try { // TODO: (@Meldiron) Remove after 1.7.x migration $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; @@ -1100,10 +1100,11 @@ Http::init() $dbForPlatform->createDocument('rules', $document); Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); - $queueForCertificates - ->setDomain($document) - ->setSkipRenewCheck(true) - ->trigger(); + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $console, + domain: $document, + skipRenewCheck: true, + )); } catch (Duplicate $e) { Console::info('Certificate already exists'); } finally { diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index bd54a8300b..5567281e67 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -3,15 +3,17 @@ use Appwrite\Auth\Key; use Appwrite\Auth\MFA\Type\TOTP; use Appwrite\Bus\Events\RequestCompleted; -use Appwrite\Event\Audit; use Appwrite\Event\Build; +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\Mail; +use Appwrite\Event\Message\Audit as AuditMessage; use Appwrite\Event\Message\Usage as UsageMessage; use Appwrite\Event\Messaging; +use Appwrite\Event\Publisher\Audit; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; @@ -88,7 +90,7 @@ Http::init() ->inject('request') ->inject('dbForPlatform') ->inject('dbForProject') - ->inject('queueForAudits') + ->inject('auditContext') ->inject('project') ->inject('user') ->inject('session') @@ -97,7 +99,7 @@ Http::init() ->inject('team') ->inject('apiKey') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { + ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { $route = $utopia->getRoute(); if ($route === null) { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); @@ -193,7 +195,7 @@ Http::init() 'name' => $apiKey->getName(), ]); - $queueForAudits->setUser($user); + $auditContext->user = $user; } // For standard keys, update last accessed time @@ -264,7 +266,7 @@ Http::init() API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION, default => ACTIVITY_TYPE_KEY_PROJECT, }); - $queueForAudits->setUser($userClone); + $auditContext->user = $userClone; } // Apply permission @@ -486,7 +488,7 @@ Http::init() ->inject('user') ->inject('queueForEvents') ->inject('queueForMessaging') - ->inject('queueForAudits') + ->inject('auditContext') ->inject('queueForDeletes') ->inject('queueForDatabase') ->inject('queueForBuilds') @@ -503,7 +505,7 @@ Http::init() ->inject('telemetry') ->inject('platform') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) { $response->setUser($user); $request->setUser($user); @@ -596,13 +598,12 @@ Http::init() ->setProject($project) ->setUser($user); - $queueForAudits - ->setMode($mode) - ->setUserAgent($request->getUserAgent('')) - ->setIP($request->getIP()) - ->setHostname($request->getHostname()) - ->setEvent($route->getLabel('audits.event', '')) - ->setProject($project); + $auditContext->mode = $mode; + $auditContext->userAgent = $request->getUserAgent(''); + $auditContext->ip = $request->getIP(); + $auditContext->hostname = $request->getHostname(); + $auditContext->event = $route->getLabel('audits.event', ''); + $auditContext->project = $project; /* If a session exists, use the user associated with the session */ if (! $user->isEmpty()) { @@ -611,7 +612,7 @@ Http::init() if (empty($user->getAttribute('type'))) { $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); } - $queueForAudits->setUser($userClone); + $auditContext->user = $userClone; } /* Auto-set projects */ @@ -790,7 +791,8 @@ Http::shutdown() ->inject('project') ->inject('user') ->inject('queueForEvents') - ->inject('queueForAudits') + ->inject('auditContext') + ->inject('publisherForAudits') ->inject('usage') ->inject('publisherForUsage') ->inject('queueForDeletes') @@ -807,7 +809,7 @@ Http::shutdown() ->inject('bus') ->inject('apiKey') ->inject('mode') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -902,7 +904,7 @@ Http::shutdown() if (! empty($pattern)) { $resource = $parseLabel($pattern, $responsePayload, $requestParams, $user); if (! empty($resource) && $resource !== $pattern) { - $queueForAudits->setResource($resource); + $auditContext->resource = $resource; } } @@ -912,8 +914,8 @@ Http::shutdown() if (empty($user->getAttribute('type'))) { $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); } - $queueForAudits->setUser($userClone); - } elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) { + $auditContext->user = $userClone; + } elseif ($auditContext->user === null || $auditContext->user->isEmpty()) { /** * User in the request is empty, and no user was set for auditing previously. * This indicates: @@ -931,24 +933,21 @@ Http::shutdown() 'name' => 'Guest', ]); - $queueForAudits->setUser($user); + $auditContext->user = $user; } - if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) { + $auditUser = $auditContext->user; + if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) { /** * audits.payload is switched to default true * in order to auto audit payload for all endpoints */ $pattern = $route->getLabel('audits.payload', true); if (! empty($pattern)) { - $queueForAudits->setPayload($responsePayload); + $auditContext->payload = $responsePayload; } - foreach ($queueForEvents->getParams() as $key => $value) { - $queueForAudits->setParam($key, $value); - } - - $queueForAudits->trigger(); + $publisherForAudits->enqueue(AuditMessage::fromContext($auditContext)); } if (! empty($queueForDeletes->getType())) { diff --git a/app/http.php b/app/http.php index 67da67376d..afcc2d2d0f 100644 --- a/app/http.php +++ b/app/http.php @@ -72,8 +72,6 @@ $swooleAdapter = new Server( container: $container, ); -$container->set('container', fn () => fn () => $swooleAdapter->getContainer()); - $http = $swooleAdapter->getServer(); /** @@ -533,6 +531,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files } $requestContainer = $swooleAdapter->getContainer(); + $requestContainer->set('container', fn () => $requestContainer); $requestContainer->set('request', fn () => $request); $requestContainer->set('response', fn () => $response); diff --git a/app/init/resources.php b/app/init/resources.php index 32d6e0a45f..d1bb7584bf 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1,8 +1,11 @@ set('publisherMessaging', function (Publisher $publisher) { $container->set('publisherWebhooks', function (Publisher $publisher) { return $publisher; }, ['publisher']); +$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher( + $publisher, + new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME)) +), ['publisher']); +$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher( + $publisher, + new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME)) +), ['publisher']); +$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher( + $publisher, + new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) +), ['publisher']); $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher( $publisher, new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME)) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 63e58e92f7..3f6196c460 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -4,9 +4,8 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Key; use Appwrite\Databases\TransactionState; -use Appwrite\Event\Audit as AuditEvent; use Appwrite\Event\Build; -use Appwrite\Event\Certificate; +use Appwrite\Event\Context\Audit as AuditContext; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -14,7 +13,6 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Realtime; -use Appwrite\Event\Screenshot; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception; use Appwrite\Functions\EventProcessor; @@ -128,9 +126,6 @@ return function (Container $container): void { $container->set('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); - $container->set('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); - }, ['publisher']); $container->set('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); @@ -149,18 +144,13 @@ return function (Container $container): void { $container->set('usage', function () { return new UsageContext(); }, []); - $container->set('queueForAudits', function (Publisher $publisher) { - return new AuditEvent($publisher); - }, ['publisher']); + $container->set('auditContext', fn () => new AuditContext(), []); $container->set('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); }, ['publisher']); $container->set('eventProcessor', function () { return new EventProcessor(); }, []); - $container->set('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); - }, ['publisher']); $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) { $adapter = new DatabasePool($pools->get('console')); $database = new Database($adapter, $cache); diff --git a/app/init/worker/message.php b/app/init/worker/message.php index f893c84858..c505d4cb3a 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -1,8 +1,6 @@ set('queueForScreenshots', function (Publisher $publisher) { - return new Screenshot($publisher); - }, ['publisher']); - $container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); @@ -323,10 +316,6 @@ return function (Container $container): void { return new Event($publisher); }, ['publisher']); - $container->set('queueForAudits', function (Publisher $publisher) { - return new Audit($publisher); - }, ['publisher']); - $container->set('queueForWebhooks', function (Publisher $publisher) { return new Webhook($publisher); }, ['publisher']); @@ -339,10 +328,6 @@ return function (Container $container): void { return new Realtime(); }, []); - $container->set('queueForCertificates', function (Publisher $publisher) { - return new Certificate($publisher); - }, ['publisher']); - $container->set('deviceForSites', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); diff --git a/composer.lock b/composer.lock index 4afe17abff..bc3d9d30bf 100644 --- a/composer.lock +++ b/composer.lock @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.20", + "version": "5.3.21", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24" + "reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24", - "reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d", + "reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d", "shasum": "" }, "require": { @@ -3903,9 +3903,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.20" + "source": "https://github.com/utopia-php/database/tree/5.3.21" }, - "time": "2026-04-10T08:27:41+00:00" + "time": "2026-04-10T12:38:57+00:00" }, { "name": "utopia-php/detector", diff --git a/docker-compose.yml b/docker-compose.yml index aa2bfdd16a..391d71fb48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1288,6 +1288,7 @@ services: image: mongo:8.2.5 container_name: appwrite-mongodb <<: *x-logging + restart: on-failure:3 networks: - appwrite volumes: diff --git a/src/Appwrite/Event/Context/Audit.php b/src/Appwrite/Event/Context/Audit.php new file mode 100644 index 0000000000..1d41890476 --- /dev/null +++ b/src/Appwrite/Event/Context/Audit.php @@ -0,0 +1,34 @@ +project === null + && $this->user === null + && $this->mode === '' + && $this->userAgent === '' + && $this->ip === '' + && $this->hostname === '' + && $this->event === '' + && $this->resource === '' + && $this->payload === []; + } +} diff --git a/src/Appwrite/Event/Message/Audit.php b/src/Appwrite/Event/Message/Audit.php new file mode 100644 index 0000000000..ae5831c3b9 --- /dev/null +++ b/src/Appwrite/Event/Message/Audit.php @@ -0,0 +1,71 @@ + [ + '$id' => $this->project->getId(), + '$sequence' => $this->project->getSequence(), + 'database' => $this->project->getAttribute('database', ''), + ], + 'user' => $this->user->getArrayCopy(), + 'payload' => $this->payload, + 'resource' => $this->resource, + 'mode' => $this->mode, + 'ip' => $this->ip, + 'userAgent' => $this->userAgent, + 'event' => $this->event, + 'hostname' => $this->hostname, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + event: $data['event'] ?? '', + payload: $data['payload'] ?? [], + project: new Document($data['project'] ?? []), + user: new Document($data['user'] ?? []), + resource: $data['resource'] ?? '', + mode: $data['mode'] ?? '', + ip: $data['ip'] ?? '', + userAgent: $data['userAgent'] ?? '', + hostname: $data['hostname'] ?? '', + ); + } + + public static function fromContext(AuditContext $context): static + { + return new self( + event: $context->event, + payload: $context->payload, + project: $context->project ?? new Document(), + user: $context->user ?? new Document(), + resource: $context->resource, + mode: $context->mode, + ip: $context->ip, + userAgent: $context->userAgent, + hostname: $context->hostname, + ); + } +} diff --git a/src/Appwrite/Event/Message/Certificate.php b/src/Appwrite/Event/Message/Certificate.php new file mode 100644 index 0000000000..a189bb8187 --- /dev/null +++ b/src/Appwrite/Event/Message/Certificate.php @@ -0,0 +1,43 @@ + [ + '$id' => $this->project->getId(), + '$sequence' => $this->project->getSequence(), + 'database' => $this->project->getAttribute('database', ''), + ], + 'domain' => $this->domain->getArrayCopy(), + 'skipRenewCheck' => $this->skipRenewCheck, + 'validationDomain' => $this->validationDomain, + 'action' => $this->action, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + project: new Document($data['project'] ?? []), + domain: new Document($data['domain'] ?? []), + skipRenewCheck: $data['skipRenewCheck'] ?? false, + validationDomain: $data['validationDomain'] ?? null, + action: $data['action'] ?? \Appwrite\Event\Certificate::ACTION_GENERATION, + ); + } +} diff --git a/src/Appwrite/Event/Message/Screenshot.php b/src/Appwrite/Event/Message/Screenshot.php new file mode 100644 index 0000000000..a06cdfbfc0 --- /dev/null +++ b/src/Appwrite/Event/Message/Screenshot.php @@ -0,0 +1,34 @@ + [ + '$id' => $this->project->getId(), + '$sequence' => $this->project->getSequence(), + 'database' => $this->project->getAttribute('database', ''), + ], + 'deploymentId' => $this->deploymentId, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + project: new Document($data['project'] ?? []), + deploymentId: $data['deploymentId'] ?? '', + ); + } +} diff --git a/src/Appwrite/Event/Publisher/Audit.php b/src/Appwrite/Event/Publisher/Audit.php new file mode 100644 index 0000000000..daa9a01fce --- /dev/null +++ b/src/Appwrite/Event/Publisher/Audit.php @@ -0,0 +1,35 @@ +publish($this->queue, $message); + } catch (\Throwable $th) { + Console::error('[Audit] Failed to publish audit message: ' . $th->getMessage()); + + return false; + } + } + + public function getSize(bool $failed = false): int + { + return $this->getQueueSize($this->queue, $failed); + } +} diff --git a/src/Appwrite/Event/Publisher/Certificate.php b/src/Appwrite/Event/Publisher/Certificate.php new file mode 100644 index 0000000000..472fb0d701 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Certificate.php @@ -0,0 +1,27 @@ +publish($this->queue, $message); + } + + public function getSize(bool $failed = false): int + { + return $this->getQueueSize($this->queue, $failed); + } +} diff --git a/src/Appwrite/Event/Publisher/Screenshot.php b/src/Appwrite/Event/Publisher/Screenshot.php new file mode 100644 index 0000000000..2a0fa1e0f8 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Screenshot.php @@ -0,0 +1,27 @@ +publish($this->queue, $message); + } + + public function getSize(bool $failed = false): int + { + return $this->getQueueSize($this->queue, $failed); + } +} diff --git a/src/Appwrite/GraphQL/ResolverLock.php b/src/Appwrite/GraphQL/ResolverLock.php new file mode 100644 index 0000000000..b1cdcf3d53 --- /dev/null +++ b/src/Appwrite/GraphQL/ResolverLock.php @@ -0,0 +1,55 @@ +channel = new Channel(1); + } + + /** + * Acquire the lock. Re-entering from the same coroutine only + * increments depth to avoid self-deadlock. + */ + public function acquire(): void + { + $cid = Coroutine::getCid(); + + if ($this->owner === $cid) { + $this->depth++; + return; + } + + $this->channel->push(true); + $this->owner = $cid; + $this->depth = 1; + } + + /** + * Release the lock. + */ + public function release(): void + { + if ($this->owner !== Coroutine::getCid()) { + return; + } + + $this->depth--; + + if ($this->depth > 0) { + return; + } + + $this->owner = null; + $this->channel->pop(); + } +} diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 0de97f4953..cabb357607 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -6,6 +6,7 @@ use Appwrite\GraphQL\Exception as GQLException; use Appwrite\Promises\Swoole; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Utopia\DI\Container; use Utopia\Http\Exception; use Utopia\Http\Http; use Utopia\Http\Route; @@ -13,6 +14,75 @@ use Utopia\System\System; class Resolvers { + /** + * Request-scoped locks keyed by the per-request GraphQL Http instance. + * + * @var array + */ + private static array $locks = []; + + /** + * Preserve response side effects that callers depend on, such as session + * cookies created by account auth routes. + */ + private static function mergeResponseSideEffects(Response $from, Response $to): void + { + foreach ($from->getCookies() as $cookie) { + $to->removeCookie($cookie['name']); + $to->addCookie( + $cookie['name'], + $cookie['value'], + $cookie['expire'], + $cookie['path'], + $cookie['domain'], + $cookie['secure'], + $cookie['httponly'], + $cookie['samesite'] + ); + } + + $headers = $from->getHeaders(); + $fallbackCookies = $headers['X-Fallback-Cookies'] ?? null; + if ($fallbackCookies === null) { + return; + } + + $to->removeHeader('X-Fallback-Cookies'); + foreach ((array) $fallbackCookies as $value) { + $to->addHeader('X-Fallback-Cookies', $value); + } + } + + /** + * Get the current request container. + */ + private static function getResolverContainer(Http $utopia): Container + { + $container = $utopia->getResource('container'); + + if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) { + /** @var Container $container */ + return $container; + } + + /** @var callable(): Container $container */ + return $container(); + } + + /** + * Get the request-scoped lock shared by GraphQL resolver coroutines + * for the current HTTP request. + */ + private static function getLock(Http $utopia): ResolverLock + { + $key = \spl_object_hash($utopia); + if (!isset(self::$locks[$key])) { + self::$locks[$key] = new ResolverLock(); + } + + return self::$locks[$key]; + } + /** * Create a resolver for a given API {@see Route}. * @@ -24,34 +94,39 @@ class Resolvers Http $utopia, ?Route $route, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $route, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $path = $route->getPath(); - foreach ($args as $key => $value) { - if (\str_contains($path, '/:' . $key)) { - $path = \str_replace(':' . $key, $value, $path); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + prepareRequest: static function (Request $request) use ($route, $args): void { + $path = $route->getPath(); + foreach ($args as $key => $value) { + if (\str_contains($path, '/:' . $key)) { + $path = \str_replace(':' . $key, $value, $path); + } + } + + $request->setMethod($route->getMethod()); + $request->setURI($path); + + switch ($route->getMethod()) { + case 'GET': + $request->setQueryString($args); + break; + default: + $request->setPayload($args); + break; } } - - $request->setMethod($route->getMethod()); - $request->setURI($path); - - switch ($route->getMethod()) { - case 'GET': - $request->setQueryString($args); - break; - default: - $request->setPayload($args); - break; - } - - self::resolve($utopia, $request, $response, $resolve, $reject); - } - ); + ); + }); } /** @@ -91,18 +166,23 @@ class Resolvers string $collectionId, callable $url, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $request->setMethod('GET'); - $request->setURI($url($databaseId, $collectionId, $args)); - - self::resolve($utopia, $request, $response, $resolve, $reject); - } - ); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void { + $request->setMethod('GET'); + $request->setURI($url($databaseId, $collectionId, $args)); + } + ); + }); } /** @@ -122,23 +202,29 @@ class Resolvers callable $url, callable $params, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $request->setMethod('GET'); - $request->setURI($url($databaseId, $collectionId, $args)); - $request->setQueryString($params($databaseId, $collectionId, $args)); + $beforeResolve = function ($payload) { + return $payload['documents']; + }; - $beforeResolve = function ($payload) { - return $payload['documents']; - }; - - self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve); - } - ); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + beforeResolve: $beforeResolve, + prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void { + $request->setMethod('GET'); + $request->setURI($url($databaseId, $collectionId, $args)); + $request->setQueryString($params($databaseId, $collectionId, $args)); + } + ); + }); } /** @@ -158,19 +244,24 @@ class Resolvers callable $url, callable $params, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $request->setMethod('POST'); - $request->setURI($url($databaseId, $collectionId, $args)); - $request->setPayload($params($databaseId, $collectionId, $args)); - - self::resolve($utopia, $request, $response, $resolve, $reject); - } - ); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void { + $request->setMethod('POST'); + $request->setURI($url($databaseId, $collectionId, $args)); + $request->setPayload($params($databaseId, $collectionId, $args)); + } + ); + }); } /** @@ -190,19 +281,24 @@ class Resolvers callable $url, callable $params, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $request->setMethod('PATCH'); - $request->setURI($url($databaseId, $collectionId, $args)); - $request->setPayload($params($databaseId, $collectionId, $args)); - - self::resolve($utopia, $request, $response, $resolve, $reject); - } - ); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void { + $request->setMethod('PATCH'); + $request->setURI($url($databaseId, $collectionId, $args)); + $request->setPayload($params($databaseId, $collectionId, $args)); + } + ); + }); } /** @@ -220,18 +316,23 @@ class Resolvers string $collectionId, callable $url, ): callable { - return static fn ($type, $args, $context, $info) => new Swoole( - function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { - $utopia = $utopia->getResource('utopia:graphql'); - $request = $utopia->getResource('request'); - $response = $utopia->getResource('response'); + return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) { + $utopia = $utopia->getResource('utopia:graphql'); + $request = $utopia->getResource('request'); + $response = $utopia->getResource('response'); - $request->setMethod('DELETE'); - $request->setURI($url($databaseId, $collectionId, $args)); - - self::resolve($utopia, $request, $response, $resolve, $reject); - } - ); + self::resolve( + $utopia, + $request, + $response, + $resolve, + $reject, + prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void { + $request->setMethod('DELETE'); + $request->setURI($url($databaseId, $collectionId, $args)); + } + ); + }); } /** @@ -241,7 +342,7 @@ class Resolvers * @param callable $resolve * @param callable $reject * @param callable|null $beforeResolve - * @param callable|null $beforeReject + * @param callable|null $prepareRequest * @return void * @throws Exception */ @@ -252,45 +353,67 @@ class Resolvers callable $resolve, callable $reject, ?callable $beforeResolve = null, - ?callable $beforeReject = null, + ?callable $prepareRequest = null, ): void { - // Drop json content type so post args are used directly - if (\str_starts_with($request->getHeader('content-type'), 'application/json')) { - $request->removeHeader('content-type'); - } + $lock = self::getLock($utopia); - $request = clone $request; - $utopia->setResource('request', static fn () => $request); - $response->setContentType(Response::CONTENT_TYPE_NULL); - $response->clearSent(); + $lock->acquire(); $original = $utopia->getRoute(); - try { - $route = $utopia->match($request, fresh: true); + $request = clone $request; - $utopia->execute($route, $request, $response); - } catch (\Throwable $e) { - if ($beforeReject) { - $e = $beforeReject($e); + // Drop json content type so post args are used directly. + if (\str_starts_with($request->getHeader('content-type'), 'application/json')) { + $request->removeHeader('content-type'); } + + if ($prepareRequest) { + $prepareRequest($request); + } + + /** @var Response $resolverResponse */ + $resolverResponse = clone $utopia->getResource('response'); + $container = self::getResolverContainer($utopia); + $container->set('request', static fn () => $request); + $container->set('response', static fn () => $resolverResponse); + $resolverResponse->setContentType(Response::CONTENT_TYPE_NULL); + $resolverResponse->setSent(false); + + $route = $utopia->match($request, fresh: true); + $request->setRoute($route); + + $utopia->execute($route, $request, $resolverResponse); + + self::mergeResponseSideEffects($resolverResponse, $response); + + if ($resolverResponse->isSent()) { + $response + ->setStatusCode($resolverResponse->getStatusCode()) + ->setSent(true); + + $resolve(null); + return; + } + + $payload = $resolverResponse->getPayload(); + $statusCode = $resolverResponse->getStatusCode(); + } catch (\Throwable $e) { $reject($e); return; } finally { if ($original !== null) { $utopia->setRoute($original); } + + $lock->release(); + unset(self::$locks[\spl_object_hash($utopia)]); } - $payload = $response->getPayload(); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) { - if ($beforeReject) { - $payload = $beforeReject($payload); - } + if ($statusCode < 200 || $statusCode >= 400) { $reject(new GQLException( message: $payload['message'], - code: $response->getStatusCode() + code: $statusCode )); return; } 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 38c84c4ae1..24cba578a9 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 @@ -50,6 +50,11 @@ class Create extends Action return UtopiaResponse::MODEL_DOCUMENT_LIST; } + protected function getSupportForEmptyDocument() + { + return false; + } + public function __construct() { $this @@ -139,30 +144,42 @@ class Create extends Action ->inject('eventProcessor') ->callback($this->action(...)); } + public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void { $data = \is_string($data) ? \json_decode($data, true) : $data; + $supportsEmptyDocument = $this->getSupportForEmptyDocument(); + $hasData = !empty($data); + $hasDocuments = !empty($documents); + /** * Determine which internal path to call, single or bulk */ - if (empty($data) && empty($documents)) { + if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) { // No single or bulk documents provided throw new Exception($this->getMissingDataException()); } - if (!empty($data) && !empty($documents)) { + + // When empty documents are supported, an empty payload should still be treated as single create. + if ($supportsEmptyDocument && !$hasData && !$hasDocuments) { + $data = []; + $hasData = true; + } + + if ($hasData && $hasDocuments) { // Both single and bulk documents provided throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup()); } - if (!empty($data) && empty($documentId)) { + if ($hasData && empty($documentId)) { // Single document provided without document ID $document = $this->isCollectionsAPI() ? 'Document' : 'Row'; $message = "$document ID is required when creating a single " . strtolower($document) . '.'; throw new Exception($this->getMissingDataException(), $message); } - if (!empty($documents) && !empty($documentId)) { + if ($hasDocuments && !empty($documentId)) { // Bulk documents provided with document ID $documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId'; throw new Exception( @@ -170,13 +187,13 @@ class Create extends Action "Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.' ); } - if (!empty($documents) && !empty($permissions)) { + if ($hasDocuments && !empty($permissions)) { // Bulk documents provided with permissions throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead'); } - $isBulk = true; - if (!empty($data)) { + $isBulk = $hasDocuments; + if ($hasData) { // Single document provided, convert to single item array // But remember that it was single to respond with a single document $isBulk = false; 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 039a05ff50..532ae826e2 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 @@ -34,6 +34,12 @@ class Create extends DocumentCreate return UtopiaResponse::MODEL_DOCUMENT_LIST; } + protected function getSupportForEmptyDocument() + { + return true; + } + + public function __construct() { $this diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php deleted file mode 100644 index cc7fe41555..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Logs/XList.php +++ /dev/null @@ -1,59 +0,0 @@ -setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs') - ->desc('List document logs') - ->groups(['api', 'database']) - ->label('scope', 'documents.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'documentsDB', - group: 'logs', - name: 'listDocumentLogs', - description: '/docs/references/documentsdb/get-document-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: SwooleResponse::STATUS_CODE_OK, - model: $this->getResponseModel(), - ) - ], - contentType: ContentType::JSON, - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID.') - ->param('documentId', '', new UID(), 'Document ID.') - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('response') - ->inject('dbForProject') - ->inject('getDatabasesDB') - ->inject('locale') - ->inject('geodb') - ->inject('authorization') - ->inject('audit') - ->callback($this->action(...)); - } -} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php deleted file mode 100644 index 51695ea165..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Logs/XList.php +++ /dev/null @@ -1,58 +0,0 @@ -setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs') - ->desc('List collection logs') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'documentsDB', - group: $this->getSdkGroup(), - name: 'listCollectionLogs', - description: '/docs/references/documentsdb/get-collection-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: SwooleResponse::STATUS_CODE_OK, - model: $this->getResponseModel(), - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) - ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject']) - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('response') - ->inject('dbForProject') - ->inject('locale') - ->inject('geodb') - ->inject('authorization') - ->inject('audit') - ->callback($this->action(...)); - } -} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php deleted file mode 100644 index dea9d30119..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Logs/XList.php +++ /dev/null @@ -1,59 +0,0 @@ -setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs') - ->desc('List document logs') - ->groups(['api', 'database']) - ->label('scope', 'documents.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'vectorsDB', - group: 'logs', - name: 'listDocumentLogs', - description: '/docs/references/vectorsdb/get-document-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: SwooleResponse::STATUS_CODE_OK, - model: $this->getResponseModel(), - ) - ], - contentType: ContentType::JSON, - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID.') - ->param('documentId', '', new UID(), 'Document ID.') - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('response') - ->inject('dbForProject') - ->inject('getDatabasesDB') - ->inject('locale') - ->inject('geodb') - ->inject('authorization') - ->inject('audit') - ->callback($this->action(...)); - } -} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php deleted file mode 100644 index cd0e45eb47..0000000000 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Logs/XList.php +++ /dev/null @@ -1,57 +0,0 @@ -setHttpMethod(self::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs') - ->desc('List collection logs') - ->groups(['api', 'database']) - ->label('scope', 'collections.read') - ->label('resourceType', RESOURCE_TYPE_DATABASES) - ->label('sdk', new Method( - namespace: 'vectorsDB', - group: $this->getSdkGroup(), - name: 'listCollectionLogs', - description: '/docs/references/vectorsdb/get-collection-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: SwooleResponse::STATUS_CODE_OK, - model: $this->getResponseModel(), - ) - ], - contentType: ContentType::JSON - )) - ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID.') - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('response') - ->inject('dbForProject') - ->inject('locale') - ->inject('geodb') - ->inject('authorization') - ->inject('audit') - ->callback($this->action(...)); - } -} diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php index a1e3538cac..5d41ed3e2b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/DocumentsDB.php @@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow; -use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows; @@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes; -use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage; use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables; @@ -69,7 +67,6 @@ class DocumentsDB extends Base $service->addAction(UpdateTable::getName(), new UpdateTable()); $service->addAction(DeleteTable::getName(), new DeleteTable()); $service->addAction(ListTables::getName(), new ListTables()); - $service->addAction(ListTableLogs::getName(), new ListTableLogs()); $service->addAction(GetTableUsage::getName(), new GetTableUsage()); } @@ -92,7 +89,6 @@ class DocumentsDB extends Base $service->addAction(DeleteRow::getName(), new DeleteRow()); $service->addAction(DeleteRows::getName(), new DeleteRows()); $service->addAction(ListRows::getName(), new ListRows()); - $service->addAction(ListRowLogs::getName(), new ListRowLogs()); $service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn()); $service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn()); } diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php index 5d12b14b1a..fe96d51d20 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorsDB.php @@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument; -use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments; @@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes; -use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage; use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections; @@ -69,7 +67,6 @@ class VectorsDB extends Base $service->addAction(UpdateCollection::getName(), new UpdateCollection()); $service->addAction(DeleteCollection::getName(), new DeleteCollection()); $service->addAction(ListCollections::getName(), new ListCollections()); - $service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs()); $service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage()); } @@ -92,7 +89,6 @@ class VectorsDB extends Base $service->addAction(UpdateDocuments::getName(), new UpdateDocuments()); $service->addAction(UpsertDocuments::getName(), new UpsertDocuments()); $service->addAction(DeleteDocuments::getName(), new DeleteDocuments()); - $service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs()); } private function registerTransactionActions(Service $service): void diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index c6c4a0b38c..0071b03d2d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -6,9 +6,9 @@ use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Message\Usage as UsageMessage; +use Appwrite\Event\Publisher\Screenshot; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; -use Appwrite\Event\Screenshot; use Appwrite\Event\Webhook; use Appwrite\Filter\BranchDomain as BranchDomainFilter; use Appwrite\Usage\Context; @@ -58,7 +58,7 @@ class Builds extends Action ->inject('project') ->inject('dbForPlatform') ->inject('queueForEvents') - ->inject('queueForScreenshots') + ->inject('publisherForScreenshots') ->inject('queueForWebhooks') ->inject('queueForFunctions') ->inject('queueForRealtime') @@ -84,7 +84,7 @@ class Builds extends Action Document $project, Database $dbForPlatform, Event $queueForEvents, - Screenshot $queueForScreenshots, + Screenshot $publisherForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -126,7 +126,7 @@ class Builds extends Action $deviceForFunctions, $deviceForSites, $deviceForFiles, - $queueForScreenshots, + $publisherForScreenshots, $queueForWebhooks, $queueForFunctions, $queueForRealtime, @@ -161,7 +161,7 @@ class Builds extends Action Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, - Screenshot $queueForScreenshots, + Screenshot $publisherForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -1120,10 +1120,10 @@ class Builds extends Action /** Screenshot site */ if ($resource->getCollection() === 'sites') { - $queueForScreenshots - ->setDeploymentId($deployment->getId()) - ->setProject($project) - ->trigger(); + $publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot( + project: $project, + deploymentId: $deployment->getId(), + )); Console::log('Site screenshot queued'); } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php index d1f2b0d0bf..bac72b993f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Workers; use Ahc\Jwt\JWT; +use Appwrite\Event\Message\Screenshot; use Appwrite\Event\Message\Usage as UsageMessage; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; @@ -68,9 +69,11 @@ class Screenshots extends Action throw new \Exception('Missing payload'); } + $screenshotMessage = Screenshot::fromArray($payload); + Console::log('Site screenshot started'); - $deploymentId = $payload['deploymentId'] ?? null; + $deploymentId = $screenshotMessage->deploymentId; $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Audits/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Audits/Get.php index e01e89641d..76c34a0a2a 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Audits/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Audits/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits; -use Appwrite\Event\Audit; +use Appwrite\Event\Publisher\Audit; 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('queueForAudits') + ->inject('publisherForAudits') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Audit $queueForAudits, Response $response): void + public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void { $threshold = (int) $threshold; - $size = $queueForAudits->getSize(); + $size = $publisherForAudits->getSize(); $this->assertQueueThreshold($size, $threshold); diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Certificates/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Certificates/Get.php index 6724f25094..82c45db172 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Certificates/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Certificates/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates; -use Appwrite\Event\Certificate; +use Appwrite\Event\Publisher\Certificate; 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('queueForCertificates') + ->inject('publisherForCertificates') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Certificate $queueForCertificates, Response $response): void + public function action(int|string $threshold, Certificate $publisherForCertificates, Response $response): void { $threshold = (int) $threshold; - $size = $queueForCertificates->getSize(); + $size = $publisherForCertificates->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 1f7cc0bf33..6d77cc6e16 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,19 +2,19 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed; -use Appwrite\Event\Audit; use Appwrite\Event\Build; -use Appwrite\Event\Certificate; use Appwrite\Event\Database; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; +use Appwrite\Event\Publisher\Audit; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Event\Publisher\Migration as MigrationPublisher; +use Appwrite\Event\Publisher\Screenshot; use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher; use Appwrite\Event\Publisher\Usage as UsagePublisher; -use Appwrite\Event\Screenshot; use Appwrite\Event\Webhook; use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base; use Appwrite\SDK\AuthType; @@ -75,17 +75,17 @@ class Get extends Base ->inject('response') ->inject('queueForDatabase') ->inject('queueForDeletes') - ->inject('queueForAudits') + ->inject('publisherForAudits') ->inject('queueForMails') ->inject('queueForFunctions') ->inject('publisherForStatsResources') ->inject('publisherForUsage') ->inject('queueForWebhooks') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForBuilds') ->inject('queueForMessaging') ->inject('publisherForMigrations') - ->inject('queueForScreenshots') + ->inject('publisherForScreenshots') ->callback($this->action(...)); } @@ -95,32 +95,32 @@ class Get extends Base Response $response, Database $queueForDatabase, Delete $queueForDeletes, - Audit $queueForAudits, + Audit $publisherForAudits, Mail $queueForMails, Func $queueForFunctions, StatsResourcesPublisher $publisherForStatsResources, UsagePublisher $publisherForUsage, Webhook $queueForWebhooks, - Certificate $queueForCertificates, + Certificate $publisherForCertificates, Build $queueForBuilds, Messaging $queueForMessaging, MigrationPublisher $publisherForMigrations, - Screenshot $queueForScreenshots, + Screenshot $publisherForScreenshots, ): void { $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_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits, + System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits, System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails, System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions, System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources, System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage, System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks, - System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, + System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds, - System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots, + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots, System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations, }; diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Logs/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Logs/Get.php index dd05aebc39..0a655662de 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Logs/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Logs/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs; -use Appwrite\Event\Audit; +use Appwrite\Event\Publisher\Audit; 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('queueForAudits') + ->inject('publisherForAudits') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Audit $queueForAudits, Response $response): void + public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void { $threshold = (int) $threshold; - $size = $queueForAudits->getSize(); + $size = $publisherForAudits->getSize(); $this->assertQueueThreshold($size, $threshold); 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 bfa62ef920..a6a3e44194 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; @@ -62,7 +62,7 @@ class Create extends Action ->param('domain', null, new ValidatorDomain(), 'Domain name.') ->inject('response') ->inject('project') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('platform') @@ -70,7 +70,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log) + public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log) { $this->validateDomainRestrictions($domain, $platform); @@ -114,13 +114,14 @@ class Create extends Action } if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $project, + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); } $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 a61ce80c4b..4a8bd4897e 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; @@ -66,7 +66,7 @@ class Create extends Action ->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true) ->inject('response') ->inject('project') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('dbForProject') @@ -75,7 +75,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) + public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { $this->validateDomainRestrictions($domain, $platform); @@ -132,13 +132,14 @@ class Create extends Action } if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $project, + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); } $queueForEvents->setParam('ruleId', $rule->getId()); 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 95c29f48e8..8a265ba5bb 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; @@ -69,7 +69,7 @@ class Create extends Action ->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.') ->inject('response') ->inject('project') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('dbForProject') @@ -78,7 +78,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) + public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { $this->validateDomainRestrictions($domain, $platform); @@ -136,13 +136,14 @@ class Create extends Action } if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $project, + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); } $queueForEvents->setParam('ruleId', $rule->getId()); 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 ba99cefb42..a9dfa93a49 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; @@ -66,7 +66,7 @@ class Create extends Action ->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true) ->inject('response') ->inject('project') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('dbForProject') @@ -75,7 +75,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) + public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { $this->validateDomainRestrictions($domain, $platform); @@ -132,13 +132,14 @@ class Create extends Action } if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $project, + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); } $queueForEvents->setParam('ruleId', $rule->getId()); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php index 8a0d341132..9e81f6ff18 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Proxy\Action; use Appwrite\SDK\AuthType; @@ -56,7 +56,7 @@ class Update extends Action )) ->param('ruleId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Rule ID.', false, ['dbForProject']) ->inject('response') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForEvents') ->inject('project') ->inject('dbForPlatform') @@ -67,7 +67,7 @@ class Update extends Action public function action( string $ruleId, Response $response, - Certificate $queueForCertificates, + Certificate $publisherForCertificates, Event $queueForEvents, Document $project, Database $dbForPlatform, @@ -110,12 +110,13 @@ class Update extends Action } // Issue a TLS certificate when DNS verification is successful - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $project, + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->trigger(); + ]), + )); if (!empty($certificate)) { $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php index a7d16e0a52..f5502a5986 100644 --- a/src/Appwrite/Platform/Tasks/Interval.php +++ b/src/Appwrite/Platform/Tasks/Interval.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Event\Certificate; +use Appwrite\Event\Publisher\Certificate; use DateTime; use Swoole\Coroutine\Channel; use Swoole\Process; @@ -29,16 +29,16 @@ class Interval extends Action ->desc('Schedules tasks on regular intervals by publishing them to our queues') ->inject('dbForPlatform') ->inject('getProjectDB') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->callback($this->action(...)); } - public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): void + public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): void { Console::title('Interval V1'); Console::success(APP_NAME . ' interval process v1 has started'); - $timers = $this->runTasks($dbForPlatform, $getProjectDB, $queueForCertificates); + $timers = $this->runTasks($dbForPlatform, $getProjectDB, $publisherForCertificates); $chan = new Channel(1); Process::signal(SIGTERM, function () use ($chan) { @@ -52,16 +52,16 @@ class Interval extends Action } } - public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): array + public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): array { $timers = []; $tasks = $this->getTasks(); foreach ($tasks as $task) { - $timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $queueForCertificates) { + $timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $publisherForCertificates) { $taskName = $task['name']; Span::init("interval.{$taskName}"); try { - $task['callback']($dbForPlatform, $getProjectDB, $queueForCertificates); + $task['callback']($dbForPlatform, $getProjectDB, $publisherForCertificates); } catch (\Exception $e) { Span::error($e); } finally { @@ -80,15 +80,15 @@ class Interval extends Action return [ [ 'name' => 'domainVerification', - "callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates) { - $this->verifyDomain($dbForPlatform, $queueForCertificates); + "callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates) { + $this->verifyDomain($dbForPlatform, $publisherForCertificates); }, 'interval' => $intervalDomainVerification * 1000, ] ]; } - private function verifyDomain(Database $dbForPlatform, Certificate $queueForCertificates): void + private function verifyDomain(Database $dbForPlatform, Certificate $publisherForCertificates): void { $time = DatabaseDateTime::now(); $fromTime = new DateTime('-3 days'); // Max 3 days old @@ -115,13 +115,17 @@ class Interval extends Action foreach ($rules as $rule) { try { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: new Document([ + '$id' => $rule->getAttribute('projectId', ''), + '$sequence' => $rule->getAttribute('projectInternalId', 0), + ]), + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_DOMAIN_VERIFICATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION, + )); $processed++; } catch (\Throwable $th) { $failed++; diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index c821435786..fe803f1292 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Event\Certificate; use Appwrite\Event\Delete; +use Appwrite\Event\Publisher\Certificate; use DateInterval; use DateTime; use Utopia\Console; @@ -29,12 +29,12 @@ class Maintenance extends Action ->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.') ->inject('dbForPlatform') ->inject('console') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('queueForDeletes') ->callback($this->action(...)); } - public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void + public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void { Console::title('Maintenance V1'); Console::success(APP_NAME . ' maintenance process v1 has started'); @@ -59,7 +59,7 @@ class Maintenance extends Action $delay = $next->getTimestamp() - $now->getTimestamp(); } - $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) { + $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) { $time = DatabaseDateTime::now(); Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); @@ -92,7 +92,7 @@ class Maintenance extends Action ->trigger(); $this->notifyDeleteConnections($queueForDeletes); - $this->renewCertificates($dbForPlatform, $queueForCertificates); + $this->renewCertificates($dbForPlatform, $publisherForCertificates); $this->notifyDeleteCache($cacheRetention, $queueForDeletes); $this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes); $this->notifyDeleteCSVExports($queueForDeletes); @@ -124,7 +124,7 @@ class Maintenance extends Action ->trigger(); } - private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void + private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void { $time = DatabaseDateTime::now(); @@ -158,13 +158,17 @@ class Maintenance extends Action continue; } - $queueForCertificate - ->setDomain(new Document([ + $publisherForCertificate->enqueue(new \Appwrite\Event\Message\Certificate( + project: new Document([ + '$id' => $rule->getAttribute('projectId', ''), + '$sequence' => $rule->getAttribute('projectInternalId', 0), + ]), + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); } } diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index ef8283f168..cb33836a99 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Event\Certificate; +use Appwrite\Event\Publisher\Certificate; use Utopia\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -29,11 +29,11 @@ class SSL extends Action ->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true) ->inject('console') ->inject('dbForPlatform') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->callback($this->action(...)); } - public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void + public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates): void { $domain = new Domain(!empty($domain) ? $domain : ''); if (!$domain->isKnown() || $domain->isTest()) { @@ -98,12 +98,13 @@ class SSL extends Action Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get()); } - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $domain->get() - ])) - ->setSkipRenewCheck($skipCheck) - ->trigger(); + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: $console, + domain: new Document([ + 'domain' => $domain->get(), + ]), + skipRenewCheck: $skipCheck, + )); Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get()); } diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index 6bcc85bc36..e5a7950945 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Workers; +use Appwrite\Event\Message\Audit; use Exception; use Throwable; use Utopia\Console; @@ -40,7 +41,6 @@ class Audits extends Action $this ->desc('Audits worker') ->inject('message') - ->inject('project') ->inject('getAudit') ->callback($this->action(...)); @@ -50,14 +50,13 @@ class Audits extends Action /** * @param Message $message - * @param Document $project * @param callable(Document): \Utopia\Audit\Audit $getAudit * @return Commit|NoCommit * @throws Throwable * @throws \Utopia\Database\Exception * @throws Structure */ - public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit + public function action(Message $message, callable $getAudit): Commit|NoCommit { $payload = $message->getPayload() ?? []; @@ -65,19 +64,21 @@ class Audits extends Action throw new Exception('Missing payload'); } + $auditMessage = Audit::fromArray($payload); + Console::info('Aggregating audit logs'); - $event = $payload['event'] ?? ''; + $event = $auditMessage->event; $auditPayload = ''; - if ($project->getId() === 'console') { - $auditPayload = $payload['payload'] ?? ''; + if ($auditMessage->project->getId() === 'console') { + $auditPayload = $auditMessage->payload; } - $mode = $payload['mode'] ?? ''; - $resource = $payload['resource'] ?? ''; - $userAgent = $payload['userAgent'] ?? ''; - $ip = $payload['ip'] ?? ''; - $user = new Document($payload['user'] ?? []); + $mode = $auditMessage->mode; + $resource = $auditMessage->resource; + $userAgent = $auditMessage->userAgent; + $ip = $auditMessage->ip; + $user = $auditMessage->user; $impersonatorUserId = $user->getAttribute('impersonatorUserId'); $actorUserId = $impersonatorUserId ?: $user->getId(); @@ -126,14 +127,14 @@ class Audits extends Action ]; } - if (isset($this->logs[$project->getSequence()])) { - $this->logs[$project->getSequence()]['logs'][] = $eventData; + if (isset($this->logs[$auditMessage->project->getSequence()])) { + $this->logs[$auditMessage->project->getSequence()]['logs'][] = $eventData; } else { - $this->logs[$project->getSequence()] = [ + $this->logs[$auditMessage->project->getSequence()] = [ 'project' => new Document([ - '$id' => $project->getId(), - '$sequence' => $project->getSequence(), - 'database' => $project->getAttribute('database'), + '$id' => $auditMessage->project->getId(), + '$sequence' => $auditMessage->project->getSequence(), + 'database' => $auditMessage->project->getAttribute('database'), ]), 'logs' => [$eventData] ]; diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 73509819a9..34234971d9 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -3,10 +3,10 @@ namespace Appwrite\Platform\Workers; use Appwrite\Certificates\Adapter as CertificatesAdapter; -use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Mail; +use Appwrite\Event\Publisher\Certificate; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception as AppwriteException; @@ -55,7 +55,7 @@ class Certificates extends Action ->inject('queueForWebhooks') ->inject('queueForFunctions') ->inject('queueForRealtime') - ->inject('queueForCertificates') + ->inject('publisherForCertificates') ->inject('log') ->inject('certificates') ->inject('plan') @@ -71,7 +71,7 @@ class Certificates extends Action * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime - * @param Certificate $queueForCertificates + * @param Certificate $publisherForCertificates * @param Log $log * @param CertificatesAdapter $certificates * @param array $plan @@ -88,7 +88,7 @@ class Certificates extends Action Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, - Certificate $queueForCertificates, + Certificate $publisherForCertificates, Log $log, CertificatesAdapter $certificates, array $plan, @@ -100,21 +100,22 @@ class Certificates extends Action throw new Exception('Missing payload'); } - $document = new Document($payload['domain'] ?? []); + $certificateMessage = \Appwrite\Event\Message\Certificate::fromArray($payload); + $document = $certificateMessage->domain; $domain = new Domain($document->getAttribute('domain', '')); $domainType = $document->getAttribute('domainType'); - $skipRenewCheck = $payload['skipRenewCheck'] ?? false; - $validationDomain = $payload['validationDomain'] ?? null; - $action = $payload['action'] ?? Certificate::ACTION_GENERATION; + $skipRenewCheck = $certificateMessage->skipRenewCheck; + $validationDomain = $certificateMessage->validationDomain; + $action = $certificateMessage->action; $log->addTag('domain', $domain->get()); switch ($action) { - case Certificate::ACTION_DOMAIN_VERIFICATION: - $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $authorization, $validationDomain); + case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION: + $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain); break; - case Certificate::ACTION_GENERATION: + case \Appwrite\Event\Certificate::ACTION_GENERATION: $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain); break; @@ -130,7 +131,7 @@ class Certificates extends Action * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime - * @param Certificate $queueForCertificates + * @param Certificate $publisherForCertificates * @param Log $log * @param ValidatorAuthorization $authorization * @param string|null $validationDomain @@ -146,7 +147,7 @@ class Certificates extends Action Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, - Certificate $queueForCertificates, + Certificate $publisherForCertificates, Log $log, ValidatorAuthorization $authorization, ?string $validationDomain = null @@ -188,13 +189,17 @@ class Certificates extends Action // Issue a TLS certificate when domain is verified if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { - $queueForCertificates - ->setDomain(new Document([ + $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( + project: new Document([ + '$id' => $rule->getAttribute('projectId', ''), + '$sequence' => $rule->getAttribute('projectInternalId', 0), + ]), + domain: new Document([ 'domain' => $rule->getAttribute('domain'), 'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')), - ])) - ->setAction(Certificate::ACTION_GENERATION) - ->trigger(); + ]), + action: \Appwrite\Event\Certificate::ACTION_GENERATION, + )); Console::success('Certificate generation triggered successfully.'); } diff --git a/src/Appwrite/Promises/Promise.php b/src/Appwrite/Promises/Promise.php index a58c7c29a8..579969cd7b 100644 --- a/src/Appwrite/Promises/Promise.php +++ b/src/Appwrite/Promises/Promise.php @@ -19,8 +19,7 @@ abstract class Promise return; } $resolve = function ($value) { - $this->setResult($value); - $this->setState(self::STATE_FULFILLED); + $this->setState($this->setResult($value)); }; $reject = function ($value) { $this->setResult($value); @@ -106,6 +105,11 @@ abstract class Promise } $callable = $this->isFulfilled() ? $onFulfilled : $onRejected; if (!\is_callable($callable)) { + if ($this->isRejected()) { + $reject($this->result); + return; + } + $resolve($this->result); return; } @@ -126,30 +130,36 @@ abstract class Promise abstract public static function all(iterable $promises): self; /** - * Set resolved result + * Set the resolved result, adopting nested promises while preserving + * whether the adopted promise fulfilled or rejected. * * @param mixed $value - * @return void + * @return int */ - protected function setResult(mixed $value): void + protected function setResult(mixed $value): int { if (!\is_callable([$value, 'then'])) { $this->result = $value; - return; + return self::STATE_FULFILLED; } - $resolved = false; + $state = self::STATE_PENDING; - $callable = function ($value) use (&$resolved) { - $this->setResult($value); - $resolved = true; - }; + $value->then( + function ($value) use (&$state) { + $state = $this->setResult($value); + }, + function ($value) use (&$state) { + $this->result = $value; + $state = self::STATE_REJECTED; + } + ); - $value->then($callable, $callable); - - while (!$resolved) { + while ($state === self::STATE_PENDING) { usleep(25000); } + + return $state; } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 5cd0e8366a..04d2813e30 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -613,6 +613,8 @@ class Response extends SwooleResponse throw new \Exception('Response body is not a valid JSON object.'); } + $this->payload = \is_array($data) ? $data : (array) $data; + $this ->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8) ->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); @@ -627,13 +629,12 @@ class Response extends SwooleResponse } /** - * Reset the sent flag so the response can be reused for another - * action execution (e.g. batched GraphQL queries that share one - * Response instance). + * Set the sent flag on the response. Pass false to allow reuse + * (e.g. batched GraphQL queries), true to prevent further writes. */ - public function clearSent(): static + public function setSent(bool $sent): static { - $this->sent = false; + $this->sent = $sent; return $this; } diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 951ab179b3..49f0c4c245 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -4160,178 +4160,4 @@ class AccountCustomClientTest extends Scope $this->assertEquals(401, $verification3['headers']['status-code']); } - - /** - * Test that a new email/password session is immediately usable even when - * a concurrent request re-populates the user cache between the cache purge - * and session creation. - * - * Regression test for: purging the user cache BEFORE persisting the session - * allows a concurrent request (from a different Swoole worker) to re-cache - * a stale user document that lacks the new session, causing sessionVerify - * to fail with 401 on subsequent requests using the new session. - */ - public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void - { - $projectId = $this->getProject()['$id']; - $endpoint = $this->client->getEndpoint(); - - $email = uniqid('race_', true) . getmypid() . '@localhost.test'; - $password = 'password123!'; - - // Create user - $response = $this->client->call(Client::METHOD_POST, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => 'Race Test User', - ]); - $this->assertEquals(201, $response['headers']['status-code']); - - // Login to get session A - $responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - ], [ - 'email' => $email, - 'password' => $password, - ]); - $this->assertEquals(201, $responseA['headers']['status-code']); - $sessionA = $responseA['cookies']['a_session_' . $projectId]; - - // Verify session A works - $verifyA = $this->client->call(Client::METHOD_GET, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'cookie' => 'a_session_' . $projectId . '=' . $sessionA, - ]); - $this->assertEquals(200, $verifyA['headers']['status-code']); - - /** - * Race condition scenario: - * 1. Start login B via curl_multi (non-blocking) - * 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument - * (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead) - * 3. THEN add GET requests to curl_multi - these hit different workers and - * re-cache a stale user document (without session B) during the window - * between purgeCachedDocument and createDocument - * 4. After all complete, verify session B is usable - */ - for ($attempt = 0; $attempt < 5; $attempt++) { - $loginCookies = []; - - $multi = curl_multi_init(); - - // Start login B first (alone) - $loginHandle = curl_init("{$endpoint}/account/sessions/email"); - curl_setopt_array($loginHandle, [ - CURLOPT_POST => true, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'origin: http://localhost', - 'content-type: application/json', - "x-appwrite-project: {$projectId}", - ], - CURLOPT_POSTFIELDS => \json_encode([ - 'email' => $email, - 'password' => $password, - ]), - CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) { - if (\stripos($header, 'set-cookie:') === 0) { - $cookiePart = \trim(\substr($header, 11)); - $eqPos = \strpos($cookiePart, '='); - if ($eqPos !== false) { - $name = \substr($cookiePart, 0, $eqPos); - $rest = \substr($cookiePart, $eqPos + 1); - $semiPos = \strpos($rest, ';'); - $loginCookies[$name] = $semiPos !== false - ? \substr($rest, 0, $semiPos) - : $rest; - } - } - return \strlen($header); - }, - ]); - curl_multi_add_handle($multi, $loginHandle); - - // Drive the login transfer forward and wait for the server to start - // processing the login (past hash verification + cache purge). - $deadline = \microtime(true) + 0.15; // 150ms - do { - curl_multi_exec($multi, $active); - curl_multi_select($multi, 0.005); - } while (\microtime(true) < $deadline && $active); - - // NOW add GET requests - they arrive after the cache purge - // but before session creation (which is delayed by the usleep or I/O). - $getHandles = []; - for ($i = 0; $i < 10; $i++) { - $gh = curl_init("{$endpoint}/account"); - curl_setopt_array($gh, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'origin: http://localhost', - 'content-type: application/json', - "x-appwrite-project: {$projectId}", - "cookie: a_session_{$projectId}={$sessionA}", - ], - ]); - curl_multi_add_handle($multi, $gh); - $getHandles[] = $gh; - } - - // Drive all to completion - do { - $status = curl_multi_exec($multi, $active); - if ($active) { - curl_multi_select($multi, 0.05); - } - } while ($active && $status === CURLM_OK); - - $loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE); - - curl_multi_remove_handle($multi, $loginHandle); - curl_close($loginHandle); - foreach ($getHandles as $gh) { - curl_multi_remove_handle($multi, $gh); - curl_close($gh); - } - curl_multi_close($multi); - - $this->assertEquals(201, $loginStatus, 'Login for session B should succeed'); - - $sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null; - $this->assertNotNull($sessionBCookie, 'Session B cookie should be set'); - - // THE CRITICAL CHECK: verify session B is usable immediately - $verifyB = $this->client->call(Client::METHOD_GET, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'cookie' => "a_session_{$projectId}={$sessionBCookie}", - ]); - - $this->assertEquals( - 200, - $verifyB['headers']['status-code'], - 'Session B must be immediately usable after login. ' - . 'A 401 here means a stale user cache (without the new session) was served. ' - . 'The fix is to create the session document BEFORE purging the user cache.' - ); - - // Clean up session B for next iteration - $this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'cookie' => "a_session_{$projectId}={$sessionBCookie}", - ]); - } - } } diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 2c5e587fc2..f5f1d1864c 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -11539,4 +11539,162 @@ trait DatabasesBase $this->assertEquals('Product B', $rows['body'][$this->getRecordResource()][0]['name']); $this->assertEquals(139.99, $rows['body'][$this->getRecordResource()][0]['price']); } + public function testDocumentWithEmptyPayload(): void + { + $data = $this->setupCollection(); + $databaseId = $data['databaseId']; + $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + $this->getRecordIdParam() => ID::unique(), + 'data' => [], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + if ($this->getSupportForAttributes()) { + $this->assertEquals(400, $document['headers']['status-code']); + } else { + $this->assertEquals(201, $document['headers']['status-code']); + $this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]); + $this->assertArrayNotHasKey('$collection', $document['body']); + $this->assertEquals($databaseId, $document['body']['$databaseId']); + $this->assertTrue(array_key_exists('$sequence', $document['body'])); + $this->assertIsString($document['body']['$sequence']); + + $documentId = $document['body']['$id']; + + $fetched = $this->client->call( + Client::METHOD_GET, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()) + ); + + $this->assertEquals(200, $fetched['headers']['status-code']); + $this->assertEqualsCanonicalizing([ + '$id', + '$databaseId', + '$createdAt', + '$updatedAt', + '$permissions', + '$sequence', + $this->getContainerIdResponseKey(), + ], \array_keys($fetched['body'])); + $this->assertFalse(array_key_exists('$tenant', $fetched['body'])); + + $updated = $this->client->call( + Client::METHOD_PATCH, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'data' => [ + 'status' => 'draft', + ], + ] + ); + + $this->assertEquals(200, $updated['headers']['status-code']); + $this->assertEquals('draft', $updated['body']['status']); + + $refetched = $this->client->call( + Client::METHOD_GET, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()) + ); + + $this->assertEquals(200, $refetched['headers']['status-code']); + $this->assertEquals('draft', $refetched['body']['status']); + } + } + + /** + * API keys may set $createdAt / $updatedAt; invalid strings must return 400, not 500. + * Assertions are HTTP status codes only (no error body matching). + */ + public function testInvalidDate(): void + { + $data = $this->setupAttributes(); + $databaseId = $data['databaseId']; + $invalidDatetime = '1dfs:12:55+sdf:00'; + $validUpdatedAt = '2024-01-01T00:00:00Z'; + + $apiKeyHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]; + + $documentPayload = [ + 'title' => 'Captain America', + 'releaseYear' => 1944, + 'actors' => [ + 'Chris Evans', + 'Samuel Jackson', + ], + ]; + $permissions = [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ]; + + $invalidCreate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [ + $this->getRecordIdParam() => ID::unique(), + 'data' => \array_merge($documentPayload, ['$updatedAt' => $invalidDatetime]), + 'permissions' => $permissions, + ]); + $this->assertEquals(400, $invalidCreate['headers']['status-code']); + + $document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [ + $this->getRecordIdParam() => ID::unique(), + 'data' => $documentPayload, + 'permissions' => $permissions, + ]); + $this->assertEquals(201, $document['headers']['status-code']); + $documentId = $document['body']['$id']; + $this->assertNotEmpty($documentId); + + $invalidPatch = $this->client->call( + Client::METHOD_PATCH, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + $apiKeyHeaders, + [ + 'data' => [ + '$updatedAt' => $invalidDatetime, + ], + ] + ); + $this->assertEquals(400, $invalidPatch['headers']['status-code']); + + $updated = $this->client->call( + Client::METHOD_PATCH, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + $apiKeyHeaders, + [ + 'data' => [ + '$updatedAt' => $validUpdatedAt, + ], + ] + ); + $this->assertEquals(200, $updated['headers']['status-code']); + + $refetched = $this->client->call( + Client::METHOD_GET, + $this->getRecordUrl($databaseId, $data['moviesId'], $documentId), + $apiKeyHeaders + ); + $this->assertEquals(200, $refetched['headers']['status-code']); + } }