From 34075322d735deeef5ce0df15af99941470e42d5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 8 May 2026 14:32:11 +0530 Subject: [PATCH 01/37] Migrate mails and messaging queues to publishers --- app/controllers/api/account.php | 251 ++++---- app/controllers/api/messaging.php | 75 ++- app/controllers/shared/api.php | 16 +- app/init/resources.php | 10 + app/init/resources/request.php | 8 - app/init/worker/message.php | 10 - src/Appwrite/Bus/Listeners/Mails.php | 58 +- src/Appwrite/Event/Mail.php | 576 ------------------ src/Appwrite/Event/Message/Mail.php | 66 ++ src/Appwrite/Event/Message/Messaging.php | 45 ++ src/Appwrite/Event/Messaging.php | 182 ------ src/Appwrite/Event/Publisher/Mail.php | 27 + src/Appwrite/Event/Publisher/Messaging.php | 27 + .../Http/Account/MFA/Challenges/Create.php | 78 +-- .../Health/Http/Health/Queue/Failed/Get.php | 16 +- .../Health/Http/Health/Queue/Mails/Get.php | 8 +- .../Http/Health/Queue/Messaging/Get.php | 8 +- .../Http/Project/SMTP/Tests/Create.php | 42 +- .../Modules/Teams/Http/Memberships/Create.php | 66 +- .../Platform/Tasks/ScheduleMessages.php | 23 +- .../Platform/Workers/Certificates.php | 39 +- src/Appwrite/Platform/Workers/Migrations.php | 48 +- src/Appwrite/Platform/Workers/Webhooks.php | 41 +- 23 files changed, 569 insertions(+), 1151 deletions(-) delete mode 100644 src/Appwrite/Event/Mail.php create mode 100644 src/Appwrite/Event/Message/Mail.php create mode 100644 src/Appwrite/Event/Message/Messaging.php delete mode 100644 src/Appwrite/Event/Messaging.php create mode 100644 src/Appwrite/Event/Publisher/Mail.php create mode 100644 src/Appwrite/Event/Publisher/Messaging.php diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index ac07efc72b..e01c27e45c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -13,8 +13,10 @@ use Appwrite\Bus\Events\SessionCreated; use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; -use Appwrite\Event\Mail; -use Appwrite\Event\Messaging; +use Appwrite\Event\Message\Mail as MailMessage; +use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Appwrite\Network\Validator\Redirect; @@ -2113,12 +2115,12 @@ Http::post('/v1/account/tokens/magic-url') ->inject('dbForProject') ->inject('locale') ->inject('queueForEvents') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('plan') ->inject('proofForPassword') ->inject('platform') ->inject('authorization') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) { + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2304,6 +2306,7 @@ Http::post('/v1/account/tokens/magic-url') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { @@ -2321,13 +2324,6 @@ Http::post('/v1/account/tokens/magic-url') $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (!empty($customTemplate)) { if (!empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -2348,11 +2344,17 @@ Http::post('/v1/account/tokens/magic-url') $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $projectName = $project->getAttribute('name'); @@ -2374,18 +2376,17 @@ Http::post('/v1/account/tokens/magic-url') 'team' => '', ]; - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->appendVariables($emailVariables) - ->setRecipient($email); - - if ($project->getId() === 'console') { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $email, + subject: $subject, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); $token->setAttribute('secret', $tokenSecret); @@ -2436,12 +2437,12 @@ Http::post('/v1/account/tokens/email') ->inject('dbForProject') ->inject('locale') ->inject('queueForEvents') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('plan') ->inject('proofForPassword') ->inject('proofForCode') ->inject('authorization') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) { + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2633,6 +2634,7 @@ Http::post('/v1/account/tokens/email') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { @@ -2650,13 +2652,6 @@ Http::post('/v1/account/tokens/email') $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (!empty($customTemplate)) { if (!empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -2677,11 +2672,17 @@ Http::post('/v1/account/tokens/email') $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $projectName = $project->getAttribute('name'); @@ -2717,20 +2718,18 @@ Http::post('/v1/account/tokens/email') ]); } - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setBodyTemplate($bodyTemplate) - ->appendVariables($emailVariables) - ->setRecipient($email); - - // since this is console project, set email sender name! - if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $email, + subject: $subject, + bodyTemplate: $bodyTemplate, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); $token->setAttribute('secret', $tokenSecret); @@ -2880,7 +2879,7 @@ Http::post('/v1/account/tokens/phone') ->inject('platform') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('locale') ->inject('timelimit') ->inject('usage') @@ -2888,7 +2887,7 @@ Http::post('/v1/account/tokens/phone') ->inject('store') ->inject('proofForCode') ->inject('authorization') - ->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) { + ->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -3021,11 +3020,13 @@ Http::post('/v1/account/tokens/phone') ], ]); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$phone]) - ->setProviderType(MESSAGE_TYPE_SMS); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_INTERNAL, + project: $project, + message: $messageDoc, + recipients: [$phone], + providerType: MESSAGE_TYPE_SMS, + )); $helper = PhoneNumberUtil::getInstance(); try { @@ -3681,11 +3682,11 @@ Http::post('/v1/account/recovery') ->inject('project') ->inject('platform') ->inject('locale') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('queueForEvents') ->inject('proofForToken') ->inject('authorization') - ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, MailPublisher $publisherForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3768,6 +3769,7 @@ Http::post('/v1/account/recovery') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { @@ -3785,13 +3787,6 @@ Http::post('/v1/account/recovery') $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (!empty($customTemplate)) { if (!empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -3812,11 +3807,17 @@ Http::post('/v1/account/recovery') $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $emailVariables = [ @@ -3829,19 +3830,18 @@ Http::post('/v1/account/recovery') 'team' => '' ]; - $queueForMails - ->setRecipient($profile->getAttribute('email', '')) - ->setName($profile->getAttribute('name', '')) - ->setBody($body) - ->appendVariables($emailVariables) - ->setSubject($subject) - ->setPreview($preview); - - if ($project->getId() === 'console') { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $profile->getAttribute('email', ''), + name: $profile->getAttribute('name', ''), + subject: $subject, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); $recovery->setAttribute('secret', $secret); @@ -4009,10 +4009,10 @@ Http::post('/v1/account/verifications/email') ->inject('dbForProject') ->inject('locale') ->inject('queueForEvents') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('proofForToken') ->inject('authorization') - ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) { + ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, ProofsToken $proofForToken, Authorization $authorization) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -4099,6 +4099,7 @@ Http::post('/v1/account/verifications/email') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { @@ -4116,13 +4117,6 @@ Http::post('/v1/account/verifications/email') $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (!empty($customTemplate)) { if (!empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -4143,11 +4137,17 @@ Http::post('/v1/account/verifications/email') $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $emailVariables = [ @@ -4174,20 +4174,19 @@ Http::post('/v1/account/verifications/email') ]); } - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setBodyTemplate($bodyTemplate) - ->appendVariables($emailVariables) - ->setRecipient($user->getAttribute('email')) - ->setName($user->getAttribute('name') ?? ''); - - if ($project->getId() === 'console') { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $user->getAttribute('email'), + name: $user->getAttribute('name') ?? '', + subject: $subject, + bodyTemplate: $bodyTemplate, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); $verification->setAttribute('secret', $verificationSecret); @@ -4321,7 +4320,7 @@ Http::post('/v1/account/verifications/phone') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('project') ->inject('locale') ->inject('timelimit') @@ -4329,7 +4328,7 @@ Http::post('/v1/account/verifications/phone') ->inject('plan') ->inject('proofForCode') ->inject('authorization') - ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) { + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -4398,11 +4397,13 @@ Http::post('/v1/account/verifications/phone') ], ]); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$user->getAttribute('phone')]) - ->setProviderType(MESSAGE_TYPE_SMS); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_INTERNAL, + project: $project, + message: $messageDoc, + recipients: [$user->getAttribute('phone')], + providerType: MESSAGE_TYPE_SMS, + )); $helper = PhoneNumberUtil::getInstance(); try { diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 58c6a2c29e..f59f606174 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -5,7 +5,8 @@ use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; -use Appwrite\Event\Messaging; +use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; use Appwrite\Permission; @@ -3187,9 +3188,9 @@ Http::post('/v1/messaging/messages/email') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3274,9 +3275,11 @@ Http::post('/v1/messaging/messages/email') switch ($status) { case MessageStatus::PROCESSING: - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); break; case MessageStatus::SCHEDULED: $schedule = $dbForPlatform->createDocument('schedules', new Document([ @@ -3362,9 +3365,9 @@ Http::post('/v1/messaging/messages/sms') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') - ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3418,9 +3421,11 @@ Http::post('/v1/messaging/messages/sms') switch ($status) { case MessageStatus::PROCESSING: - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); break; case MessageStatus::SCHEDULED: $schedule = $dbForPlatform->createDocument('schedules', new Document([ @@ -3498,10 +3503,10 @@ Http::post('/v1/messaging/messages/push') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') ->inject('platform') - ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) { + ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3638,9 +3643,11 @@ Http::post('/v1/messaging/messages/push') switch ($status) { case MessageStatus::PROCESSING: - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); break; case MessageStatus::SCHEDULED: $schedule = $dbForPlatform->createDocument('schedules', new Document([ @@ -3983,9 +3990,9 @@ Http::patch('/v1/messaging/messages/email/:messageId') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -4141,9 +4148,11 @@ Http::patch('/v1/messaging/messages/email/:messageId') $message = $dbForProject->updateDocument('messages', $message->getId(), $message); if ($status === MessageStatus::PROCESSING) { - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); } $queueForEvents @@ -4205,9 +4214,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -4323,9 +4332,11 @@ Http::patch('/v1/messaging/messages/sms/:messageId') $message = $dbForProject->updateDocument('messages', $message->getId(), $message); if ($status === MessageStatus::PROCESSING) { - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); } $queueForEvents @@ -4379,10 +4390,10 @@ Http::patch('/v1/messaging/messages/push/:messageId') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('project') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') ->inject('platform') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -4584,9 +4595,11 @@ Http::patch('/v1/messaging/messages/push/:messageId') $message = $dbForProject->updateDocument('messages', $message->getId(), $message); if ($status === MessageStatus::PROCESSING) { - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($message->getId()); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $project, + messageId: $message->getId(), + )); } $queueForEvents diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 14ffdc059f..8365274e98 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -8,10 +8,8 @@ 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; @@ -486,13 +484,11 @@ Http::init() ->inject('project') ->inject('user') ->inject('queueForEvents') - ->inject('queueForMessaging') ->inject('auditContext') ->inject('queueForDeletes') ->inject('queueForDatabase') ->inject('usage') ->inject('queueForFunctions') - ->inject('queueForMails') ->inject('dbForProject') ->inject('timelimit') ->inject('resourceToken') @@ -504,7 +500,7 @@ Http::init() ->inject('platform') ->inject('authorization') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, 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, callable $cacheControlForStorage) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); @@ -617,13 +613,10 @@ Http::init() /* Auto-set projects */ $queueForDeletes->setProject($project); $queueForDatabase->setProject($project); - $queueForMessaging->setProject($project); $queueForFunctions->setProject($project); - $queueForMails->setProject($project); /* Auto-set platforms */ $queueForFunctions->setPlatform($platform); - $queueForMails->setPlatform($platform); $useCache = $route->getLabel('cache', false); $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); @@ -815,7 +808,6 @@ Http::shutdown() ->inject('publisherForUsage') ->inject('queueForDeletes') ->inject('queueForDatabase') - ->inject('queueForMessaging') ->inject('queueForFunctions') ->inject('queueForWebhooks') ->inject('queueForRealtime') @@ -826,7 +818,7 @@ Http::shutdown() ->inject('bus') ->inject('apiKey') ->inject('mode') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, 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, 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(); @@ -975,10 +967,6 @@ Http::shutdown() $queueForDatabase->trigger(); } - if (! empty($queueForMessaging->getType())) { - $queueForMessaging->trigger(); - } - // Cache label $useCache = $route->getLabel('cache', false); if ($useCache) { diff --git a/app/init/resources.php b/app/init/resources.php index c5d034a125..a626b612cb 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -5,6 +5,8 @@ use Appwrite\Event\Publisher\Audit as AuditPublisher; use Appwrite\Event\Publisher\Build as BuildPublisher; use Appwrite\Event\Publisher\Certificate as CertificatePublisher; use Appwrite\Event\Publisher\Execution as ExecutionPublisher; +use Appwrite\Event\Publisher\Mail as MailPublisher; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Event\Publisher\Migration as MigrationPublisher; use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher; use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher; @@ -118,6 +120,14 @@ $container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPubl $publisher, new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME)) ), ['publisher']); +$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher( + $publisher, + new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME)) +), ['publisher']); +$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher( + $publisher, + new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME)) +), ['publisher']); /** * Platform configuration diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 9fd282c4ed..6ed377d9ae 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -9,8 +9,6 @@ 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\Messaging; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception; @@ -116,12 +114,6 @@ return function (Container $container): void { }); // Per-request queue resources (stateful, accumulate event data during request) - $container->set('queueForMessaging', function (Publisher $publisher) { - return new Messaging($publisher); - }, ['publisher']); - $container->set('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); - }, ['publisher']); $container->set('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); diff --git a/app/init/worker/message.php b/app/init/worker/message.php index 1469934ad4..791bf5edf0 100644 --- a/app/init/worker/message.php +++ b/app/init/worker/message.php @@ -4,8 +4,6 @@ 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\Messaging; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Usage\Context; @@ -333,14 +331,6 @@ return function (Container $container): void { return new EventDatabase($publisher); }, ['publisher']); - $container->set('queueForMessaging', function (Publisher $publisher) { - return new Messaging($publisher); - }, ['publisher']); - - $container->set('queueForMails', function (Publisher $publisher) { - return new Mail($publisher); - }, ['publisher']); - $container->set('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); diff --git a/src/Appwrite/Bus/Listeners/Mails.php b/src/Appwrite/Bus/Listeners/Mails.php index 9b3d68519f..eb36e0d394 100644 --- a/src/Appwrite/Bus/Listeners/Mails.php +++ b/src/Appwrite/Bus/Listeners/Mails.php @@ -4,14 +4,14 @@ namespace Appwrite\Bus\Listeners; use Appwrite\Auth\MFA\Type; use Appwrite\Bus\Events\SessionCreated; -use Appwrite\Event\Mail; +use Appwrite\Event\Message\Mail as MailMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Template\Template; use Utopia\Bus\Listener; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Locale\Locale; -use Utopia\Queue\Publisher; use Utopia\Storage\Validator\FileName; use Utopia\System\System; @@ -31,14 +31,14 @@ class Mails extends Listener { $this ->desc('Sends session alert emails') - ->inject('publisher') + ->inject('publisherForMails') ->inject('locale') ->inject('platform') ->inject('dbForProject') ->callback($this->handle(...)); } - public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void + public function handle(SessionCreated $event, MailPublisher $publisherForMails, Locale $locale, array $platform, Database $dbForProject): void { $project = new Document($event->project); @@ -124,34 +124,32 @@ class Mails extends Listener ]; } - $queueForMails = new Mail($publisher); - + $smtpConfig = []; if ($smtp['enabled'] ?? false) { - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? '') - ->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility - ->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '') - ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) - ->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '', // Includes backwards compatibility + 'replyToName' => $customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '', + 'senderEmail' => $customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + 'senderName' => $customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'), + ]; } - $queueForMails - ->setProject($project) - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl') - ->appendVariables($emailVariables) - ->setRecipient($event->user['email']); - - if ($isBranded) { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $event->user['email'], + subject: $subject, + bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl', + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $isBranded ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); } } diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php deleted file mode 100644 index 0685586c60..0000000000 --- a/src/Appwrite/Event/Mail.php +++ /dev/null @@ -1,576 +0,0 @@ -setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME)) - ->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME)); - } - - /** - * Sets subject for the mail event. - * - * @param string $subject - * @return self - */ - public function setSubject(string $subject): self - { - $this->subject = $subject; - - return $this; - } - - /** - * Returns subject for the mail event. - * - * @return string - */ - public function getSubject(): string - { - return $this->subject; - } - - /** - * Sets recipient for the mail event. - * - * @param string $recipient - * @return self - */ - public function setRecipient(string $recipient): self - { - $this->recipient = $recipient; - - return $this; - } - - /** - * Returns set recipient for mail event. - * - * @return string - */ - public function getRecipient(): string - { - return $this->recipient; - } - - /** - * Sets body for the mail event. - * - * @param string $body - * @return self - */ - public function setBody(string $body): self - { - $this->body = $body; - - return $this; - } - - /** - * Returns body for the mail event. - * - * @return string - */ - public function getBody(): string - { - return $this->body; - } - - /** - * Sets preview for the mail event. - * - * @return self - */ - public function setPreview(string $preview): self - { - $this->preview = $preview; - - return $this; - } - - /** - * Returns preview for the mail event. - * - * @return string - */ - public function getPreview(): string - { - return $this->preview; - } - - /** - * Sets name for the mail event. - * - * @param string $name - * @return self - */ - public function setName(string $name): self - { - $this->name = $name; - - return $this; - } - - /** - * Returns set name for the mail event. - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Sets bodyTemplate for the mail event. - * - * @param string $bodyTemplate - * @return self - */ - public function setBodyTemplate(string $bodyTemplate): self - { - $this->bodyTemplate = $bodyTemplate; - - return $this; - } - - /** - * Returns subject for the mail event. - * - * @return string - */ - public function getBodyTemplate(): string - { - return $this->bodyTemplate; - } - - /** - * Set SMTP Host - * - * @param string $host - * @return self - */ - public function setSmtpHost(string $host): self - { - $this->smtp['host'] = $host; - return $this; - } - - /** - * Set SMTP port - * - * @param int $port - * @return self - */ - public function setSmtpPort(int $port): self - { - $this->smtp['port'] = $port; - return $this; - } - - /** - * Set SMTP username - * - * @param string $username - * @return self - */ - public function setSmtpUsername(string $username): self - { - $this->smtp['username'] = $username; - return $this; - } - - /** - * Set SMTP password - * - * @param string $password - * @return self - */ - public function setSmtpPassword(string $password): self - { - $this->smtp['password'] = $password; - return $this; - } - - /** - * Set SMTP secure - * - * @param string $secure - * @return self - */ - public function setSmtpSecure(string $secure): self - { - $this->smtp['secure'] = $secure; - return $this; - } - - /** - * Set SMTP sender email - * - * @param string $senderEmail - * @return self - */ - public function setSmtpSenderEmail(string $senderEmail): self - { - $this->smtp['senderEmail'] = $senderEmail; - return $this; - } - - /** - * Set SMTP sender name - * - * @param string $senderName - * @return self - */ - public function setSmtpSenderName(string $senderName): self - { - $this->smtp['senderName'] = $senderName; - return $this; - } - - /** - * Set SMTP reply-to email - * - * @param string $email - * @return self - */ - public function setSmtpReplyToEmail(string $email): self - { - $this->smtp['replyToEmail'] = $email; - return $this; - } - - /** - * Set SMTP reply-to name - * - * @param string $name - * @return self - */ - public function setSmtpReplyToName(string $name): self - { - $this->smtp['replyToName'] = $name; - return $this; - } - - /** - * Get SMTP - * - * @return string - */ - public function getSmtpHost(): string - { - return $this->smtp['host'] ?? ''; - } - - /** - * Get SMTP port - * - * @return integer - */ - public function getSmtpPort(): int - { - return $this->smtp['port'] ?? 0; - } - - /** - * Get SMTP username - * - * @return string - */ - public function getSmtpUsername(): string - { - return $this->smtp['username'] ?? ''; - } - - /** - * Get SMTP password - * - * @return string - */ - public function getSmtpPassword(): string - { - return $this->smtp['password'] ?? ''; - } - - /** - * Get SMTP secure - * - * @return string - */ - public function getSmtpSecure(): string - { - return $this->smtp['secure'] ?? ''; - } - - /** - * Get SMTP sender email - * - * @return string - */ - public function getSmtpSenderEmail(): string - { - return $this->smtp['senderEmail'] ?? ''; - } - - /** - * Get SMTP sender name - * - * @return string - */ - public function getSmtpSenderName(): string - { - return $this->smtp['senderName'] ?? ''; - } - - /** - * Get SMTP reply-to email - * - * @return string - */ - public function getSmtpReplyToEmail(): string - { - return $this->smtp['replyToEmail'] ?? ''; - } - - /** - * Get SMTP reply-to name - * - * @return string - */ - public function getSmtpReplyToName(): string - { - return $this->smtp['replyToName'] ?? ''; - } - - /** - * Get Email Variables - * - * @return array - */ - public function getVariables(): array - { - return $this->variables; - } - - /** - * Set Email Variables - * - * @param array $variables - * @return self - */ - public function setVariables(array $variables): self - { - $this->variables = $variables; - return $this; - } - - /** - * Append variables to the email event. - * - * @param array $variables - * @return self - */ - public function appendVariables(array $variables): self - { - $this->variables = \array_merge($this->variables, $variables); - return $this; - } - - /** - * Set attachment - * @param string $content - * @param string $filename - * @param string $encoding - * @param string $type - * @return self - */ - public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text') - { - $this->attachment = [ - 'content' => base64_encode($content), - 'filename' => $filename, - 'encoding' => $encoding, - 'type' => $type, - ]; - return $this; - } - - /** - * Get attachment - * - * @return array - */ - public function getAttachment(): array - { - return $this->attachment; - } - - /** - * Reset attachment - * - * @return self - */ - public function resetAttachment(): self - { - $this->attachment = []; - return $this; - } - - /** - * Set sender email - * - * @param string $email - * @return self - */ - public function setSenderEmail(string $email): self - { - $this->customMailOptions['senderEmail'] = $email; - return $this; - } - - /** - * Get sender email - * - * @return string - */ - public function getSenderEmail(): string - { - return $this->customMailOptions['senderEmail'] ?? ''; - } - - /** - * Set sender name - * - * @param string $name - * @return self - */ - public function setSenderName(string $name): self - { - $this->customMailOptions['senderName'] = $name; - return $this; - } - - /** - * Get sender name - * - * @return string - */ - public function getSenderName(): string - { - return $this->customMailOptions['senderName'] ?? ''; - } - - /** - * Set reply-to email - * - * @param string $email - * @return self - */ - public function setReplyToEmail(string $email): self - { - $this->customMailOptions['replyToEmail'] = $email; - return $this; - } - - /** - * Get reply-to email - * - * @return string - */ - public function getReplyToEmail(): string - { - return $this->customMailOptions['replyToEmail'] ?? ''; - } - - /** - * Set reply-to name - * - * @param string $name - * @return self - */ - public function setReplyToName(string $name): self - { - $this->customMailOptions['replyToName'] = $name; - return $this; - } - - /** - * Get reply-to name - * - * @return string - */ - public function getReplyToName(): string - { - return $this->customMailOptions['replyToName'] ?? ''; - } - - /** - * Reset - * - * @return self - */ - public function reset(): self - { - $this->project = null; - $this->recipient = ''; - $this->name = ''; - $this->subject = ''; - $this->body = ''; - $this->variables = []; - $this->bodyTemplate = ''; - $this->attachment = []; - $this->customMailOptions = []; - return $this; - } - - /** - * Prepare the payload for the event - * - * @return array - */ - protected function preparePayload(): array - { - $platform = $this->platform; - if (empty($platform)) { - $platform = Config::getParam('platform', []); - } - - return [ - 'project' => $this->project, - 'recipient' => $this->recipient, - 'name' => $this->name, - 'subject' => $this->subject, - 'bodyTemplate' => $this->bodyTemplate, - 'body' => $this->body, - 'preview' => $this->preview, - 'smtp' => $this->smtp, - 'variables' => $this->variables, - 'attachment' => $this->attachment, - 'customMailOptions' => $this->customMailOptions, - 'events' => Event::generateEvents($this->getEvent(), $this->getParams()), - 'platform' => $platform, - ]; - } -} diff --git a/src/Appwrite/Event/Message/Mail.php b/src/Appwrite/Event/Message/Mail.php new file mode 100644 index 0000000000..aeeea8a616 --- /dev/null +++ b/src/Appwrite/Event/Message/Mail.php @@ -0,0 +1,66 @@ +platform) ? $this->platform : Config::getParam('platform', []); + + return [ + 'project' => $this->project?->getArrayCopy(), + 'recipient' => $this->recipient, + 'name' => $this->name, + 'subject' => $this->subject, + 'bodyTemplate' => $this->bodyTemplate, + 'body' => $this->body, + 'preview' => $this->preview, + 'smtp' => $this->smtp, + 'variables' => $this->variables, + 'attachment' => $this->attachment, + 'customMailOptions' => $this->customMailOptions, + 'events' => $this->events, + 'platform' => $platform, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + project: !empty($data['project']) ? new Document($data['project']) : null, + recipient: $data['recipient'] ?? '', + name: $data['name'] ?? '', + subject: $data['subject'] ?? '', + bodyTemplate: $data['bodyTemplate'] ?? '', + body: $data['body'] ?? '', + preview: $data['preview'] ?? '', + smtp: $data['smtp'] ?? [], + variables: $data['variables'] ?? [], + attachment: $data['attachment'] ?? [], + customMailOptions: $data['customMailOptions'] ?? [], + events: $data['events'] ?? [], + platform: $data['platform'] ?? [], + ); + } +} diff --git a/src/Appwrite/Event/Message/Messaging.php b/src/Appwrite/Event/Message/Messaging.php new file mode 100644 index 0000000000..7f0f918217 --- /dev/null +++ b/src/Appwrite/Event/Message/Messaging.php @@ -0,0 +1,45 @@ + $this->type, + 'project' => $this->project->getArrayCopy(), + 'user' => $this->user?->getArrayCopy(), + 'messageId' => $this->messageId, + 'message' => $this->message?->getArrayCopy(), + 'recipients' => $this->recipients, + 'providerType' => $this->providerType, + ]; + } + + public static function fromArray(array $data): static + { + return new self( + type: $data['type'] ?? '', + project: new Document($data['project'] ?? []), + user: !empty($data['user']) ? new Document($data['user']) : null, + messageId: $data['messageId'] ?? null, + message: !empty($data['message']) ? new Document($data['message']) : null, + recipients: $data['recipients'] ?? null, + providerType: $data['providerType'] ?? null, + ); + } +} diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php deleted file mode 100644 index 9895d52ec2..0000000000 --- a/src/Appwrite/Event/Messaging.php +++ /dev/null @@ -1,182 +0,0 @@ -setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME)) - ->setClass(System::getEnv('_APP_MESSAGING_CLASS_NAME', Event::MESSAGING_CLASS_NAME)); - } - - /** - * Sets type for the build event. - * - * @param string $type Can be `MESSAGE_SEND_TYPE_INTERNAL` or `MESSAGE_SEND_TYPE_EXTERNAL`. - * @return self - */ - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - - /** - * Returns set type for the function event. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * Sets recipient for the messaging event. - * - * @param string[] $recipients - * @return self - */ - public function setRecipients(array $recipients): self - { - $this->recipients = $recipients; - - return $this; - } - - /** - * Returns set recipient for messaging event. - * - * @return string[] - */ - public function getRecipient(): array - { - return $this->recipients; - } - - /** - * Sets message document for the messaging event. - * - * @param Document $message - * @return self - */ - public function setMessage(Document $message): self - { - $this->message = $message; - - return $this; - } - - /** - * Returns message document for the messaging event. - * - * @return Document - */ - public function getMessage(): Document - { - return $this->message; - } - - /** - * Sets message ID for the messaging event. - * - * @param string $messageId - * @return self - */ - public function setMessageId(string $messageId): self - { - $this->messageId = $messageId; - - return $this; - } - - /** - * Returns set message ID for the messaging event. - * - * @return string - */ - public function getMessageId(): string - { - return $this->messageId; - } - - /** - * Sets provider type for the messaging event. - * - * @param string $providerType - * @return self - */ - public function setProviderType(string $providerType): self - { - $this->providerType = $providerType; - - return $this; - } - - /** - * Returns set provider type for the messaging event. - * - * @return string - */ - public function getProviderType(): string - { - return $this->providerType; - } - - /** - * Sets Scheduled delivery time for the messaging event. - * - * @param string $scheduledAt - * @return self - */ - public function setScheduledAt(string $scheduledAt): self - { - $this->scheduledAt = $scheduledAt; - - return $this; - } - - /** - * Returns set Delivery Time for the messaging event. - * - * @return string - */ - public function getScheduledAt(): string - { - return $this->scheduledAt; - } - - /** - * Prepare the payload for the event - * - * @return array - */ - protected function preparePayload(): array - { - return [ - 'type' => $this->type, - 'project' => $this->project, - 'user' => $this->user, - 'messageId' => $this->messageId, - 'message' => $this->message, - 'recipients' => $this->recipients, - 'providerType' => $this->providerType, - ]; - } -} diff --git a/src/Appwrite/Event/Publisher/Mail.php b/src/Appwrite/Event/Publisher/Mail.php new file mode 100644 index 0000000000..16d48be044 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Mail.php @@ -0,0 +1,27 @@ +publish($queue ?? $this->queue, $message); + } + + public function getSize(bool $failed = false, ?Queue $queue = null): int + { + return $this->getQueueSize($queue ?? $this->queue, $failed); + } +} diff --git a/src/Appwrite/Event/Publisher/Messaging.php b/src/Appwrite/Event/Publisher/Messaging.php new file mode 100644 index 0000000000..69863566a1 --- /dev/null +++ b/src/Appwrite/Event/Publisher/Messaging.php @@ -0,0 +1,27 @@ +publish($queue ?? $this->queue, $message); + } + + public function getSize(bool $failed = false, ?Queue $queue = null): int + { + return $this->getQueueSize($queue ?? $this->queue, $failed); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php index 7bcc78e974..285875eb35 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php @@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges; use Appwrite\Auth\MFA\Type; use Appwrite\Detector\Detector; use Appwrite\Event\Event; -use Appwrite\Event\Mail; -use Appwrite\Event\Messaging; +use Appwrite\Event\Message\Mail as MailMessage; +use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; @@ -101,8 +103,8 @@ class Create extends Action ->inject('platform') ->inject('request') ->inject('queueForEvents') - ->inject('queueForMessaging') - ->inject('queueForMails') + ->inject('publisherForMessaging') + ->inject('publisherForMails') ->inject('timelimit') ->inject('usage') ->inject('plan') @@ -121,8 +123,8 @@ class Create extends Action array $platform, Request $request, Event $queueForEvents, - Messaging $queueForMessaging, - Mail $queueForMails, + MessagingPublisher $publisherForMessaging, + MailPublisher $publisherForMails, callable $timelimit, Context $usage, array $plan, @@ -180,16 +182,18 @@ class Create extends Action $message = $message->render(); $phone = $user->getAttribute('phone'); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage(new Document([ + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_INTERNAL, + project: $project, + message: new Document([ '$id' => $challenge->getId(), 'data' => [ 'content' => $code, ], - ])) - ->setRecipients([$phone]) - ->setProviderType(MESSAGE_TYPE_SMS); + ]), + recipients: [$phone], + providerType: MESSAGE_TYPE_SMS, + )); $helper = PhoneNumberUtil::getInstance(); try { @@ -252,6 +256,7 @@ class Create extends Action $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { @@ -269,13 +274,6 @@ class Create extends Action $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (!empty($customTemplate)) { if (!empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -296,11 +294,17 @@ class Create extends Action $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $emailVariables = [ @@ -327,20 +331,18 @@ class Create extends Action ]); } - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setBodyTemplate($bodyTemplate) - ->appendVariables($emailVariables) - ->setRecipient($user->getAttribute('email')); - - // since this is console project, set email sender name! - if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { - $queueForMails->setSenderName($platform['emailSenderName']); - } - - $queueForMails->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $user->getAttribute('email'), + subject: $subject, + bodyTemplate: $bodyTemplate, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [], + platform: $platform, + )); break; } 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 0d0a787b46..70d7713280 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 @@ -6,11 +6,11 @@ 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\Build as BuildPublisher; use Appwrite\Event\Publisher\Certificate; +use Appwrite\Event\Publisher\Mail as MailPublisher; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Event\Publisher\Migration as MigrationPublisher; use Appwrite\Event\Publisher\Screenshot; use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher; @@ -77,14 +77,14 @@ class Get extends Base ->inject('queueForDatabase') ->inject('queueForDeletes') ->inject('publisherForAudits') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('queueForFunctions') ->inject('publisherForStatsResources') ->inject('publisherForUsage') ->inject('queueForWebhooks') ->inject('publisherForCertificates') ->inject('publisherForBuilds') - ->inject('queueForMessaging') + ->inject('publisherForMessaging') ->inject('publisherForMigrations') ->inject('publisherForScreenshots') ->callback($this->action(...)); @@ -97,14 +97,14 @@ class Get extends Base Database $queueForDatabase, Delete $queueForDeletes, Audit $publisherForAudits, - Mail $queueForMails, + MailPublisher $publisherForMails, Func $queueForFunctions, StatsResourcesPublisher $publisherForStatsResources, UsagePublisher $publisherForUsage, Webhook $queueForWebhooks, Certificate $publisherForCertificates, BuildPublisher $publisherForBuilds, - Messaging $queueForMessaging, + MessagingPublisher $publisherForMessaging, MigrationPublisher $publisherForMigrations, Screenshot $publisherForScreenshots, ): void { @@ -114,7 +114,7 @@ class Get extends Base 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) => $publisherForAudits, - System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails, + System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $publisherForMails, System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions, System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources, System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage, @@ -122,7 +122,7 @@ class Get extends Base System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $publisherForBuilds, 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_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $publisherForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations, default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unknown queue name: ' . $name), }; diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php index 3b9c06b5f9..2dd36e8111 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Mails; -use Appwrite\Event\Mail; +use Appwrite\Event\Publisher\Mail as MailPublisher; 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('queueForMails') + ->inject('publisherForMails') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Mail $queueForMails, Response $response): void + public function action(int|string $threshold, MailPublisher $publisherForMails, Response $response): void { $threshold = (int) $threshold; - $size = $queueForMails->getSize(); + $size = $publisherForMails->getSize(); $this->assertQueueThreshold($size, $threshold); diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php index db2d7d7172..a2a829b1d5 100644 --- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php +++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Messaging; -use Appwrite\Event\Messaging; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; 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('queueForMessaging') + ->inject('publisherForMessaging') ->inject('response') ->callback($this->action(...)); } - public function action(int|string $threshold, Messaging $queueForMessaging, Response $response): void + public function action(int|string $threshold, MessagingPublisher $publisherForMessaging, Response $response): void { $threshold = (int) $threshold; - $size = $queueForMessaging->getSize(); + $size = $publisherForMessaging->getSize(); $this->assertQueueThreshold($size, $threshold); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php index 7095c2d2d0..8c87a41475 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php @@ -2,7 +2,8 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests; -use Appwrite\Event\Mail; +use Appwrite\Event\Message\Mail as MailMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Extend\Exception as Exception; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; @@ -67,7 +68,7 @@ class Create extends Action ->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', optional: true, deprecated: true) // Backwards compatibility ->inject('response') ->inject('project') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('plan') ->callback($this->action(...)); } @@ -87,7 +88,7 @@ class Create extends Action string $paramSecure, // Backwards compatibility Response $response, Document $project, - Mail $queueForMails, + MailPublisher $publisherForMails, array $plan ): void { // Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config. @@ -153,23 +154,24 @@ class Create extends Action ->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL); foreach ($emails as $email) { - $queueForMails - ->setSmtpHost($host) - ->setSmtpPort($port) - ->setSmtpUsername($username) - ->setSmtpPassword($password) - ->setSmtpSecure($secure) - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName) - ->setRecipient($email) - ->setName('') - ->setBodyTemplate(APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl') - ->setBody($template->render()) - ->setVariables([]) - ->setSubject($subject) - ->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $email, + subject: $subject, + bodyTemplate: APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl', + body: $template->render(), + smtp: [ + 'host' => $host, + 'port' => $port, + 'username' => $username, + 'password' => $password, + 'secure' => $secure, + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ], + )); } $response->noContent(); diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 51115b7861..d16a71780a 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -4,8 +4,10 @@ namespace Appwrite\Platform\Modules\Teams\Http\Memberships; use Appwrite\Auth\Validator\Phone; use Appwrite\Event\Event; -use Appwrite\Event\Mail; -use Appwrite\Event\Messaging; +use Appwrite\Event\Message\Mail as MailMessage; +use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; @@ -87,8 +89,8 @@ class Create extends Action ->inject('dbForProject') ->inject('authorization') ->inject('locale') - ->inject('queueForMails') - ->inject('queueForMessaging') + ->inject('publisherForMails') + ->inject('publisherForMessaging') ->inject('queueForEvents') ->inject('timelimit') ->inject('usage') @@ -98,7 +100,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken) + public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken) { $isAppUser = $user->isApp($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); @@ -345,6 +347,7 @@ class Create extends Action $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyToEmail = ''; $replyToName = ''; + $smtpConfig = []; if ($smtpEnabled) { if (! empty($smtp['senderEmail'])) { @@ -362,13 +365,6 @@ class Create extends Action $replyToName = $smtp['replyToName']; } - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - if (! empty($customTemplate)) { if (! empty($customTemplate['senderEmail'])) { $senderEmail = $customTemplate['senderEmail']; @@ -389,11 +385,17 @@ class Create extends Action $subject = $customTemplate['subject'] ?? $subject; } - $queueForMails - ->setSmtpReplyToEmail($replyToEmail) - ->setSmtpReplyToName($replyToName) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + $smtpConfig = [ + 'host' => $smtp['host'] ?? '', + 'port' => $smtp['port'] ?? '', + 'username' => $smtp['username'] ?? '', + 'password' => $smtp['password'] ?? '', + 'secure' => $smtp['secure'] ?? '', + 'replyToEmail' => $replyToEmail, + 'replyToName' => $replyToName, + 'senderEmail' => $senderEmail, + 'senderName' => $senderName, + ]; } $emailVariables = [ @@ -406,14 +408,16 @@ class Create extends Action 'project' => $projectName, ]; - $queueForMails - ->setSubject($subject) - ->setBody($body) - ->setPreview($preview) - ->setRecipient($invitee->getAttribute('email')) - ->setName($invitee->getAttribute('name', '')) - ->appendVariables($emailVariables) - ->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $invitee->getAttribute('email'), + name: $invitee->getAttribute('name', ''), + subject: $subject, + body: $body, + preview: $preview, + smtp: $smtpConfig, + variables: $emailVariables, + )); } elseif (! empty($phone)) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); @@ -431,11 +435,13 @@ class Create extends Action ], ]); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$phone]) - ->setProviderType('SMS'); + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_INTERNAL, + project: $project, + message: $messageDoc, + recipients: [$phone], + providerType: 'SMS', + )); $helper = PhoneNumberUtil::getInstance(); try { diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 57f6dd8002..23068fcb9d 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -2,8 +2,12 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Event\Messaging; +use Appwrite\Event\Event; +use Appwrite\Event\Message\Messaging as MessagingMessage; +use Appwrite\Event\Publisher\Messaging as MessagingPublisher; use Utopia\Database\Database; +use Utopia\Queue\Queue; +use Utopia\System\System; class ScheduleMessages extends ScheduleBase { @@ -40,15 +44,18 @@ class ScheduleMessages extends ScheduleBase } \go(function () use ($schedule, $scheduledAt, $dbForPlatform) { - $queueForMessaging = new Messaging($this->publisherMessaging); - $this->updateProjectAccess($schedule['project'], $dbForPlatform); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_EXTERNAL) - ->setMessageId($schedule['resourceId']) - ->setProject($schedule['project']) - ->trigger(); + $publisherForMessaging = new MessagingPublisher( + $this->publisherMessaging, + new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME)) + ); + + $publisherForMessaging->enqueue(new MessagingMessage( + type: MESSAGE_SEND_TYPE_EXTERNAL, + project: $schedule['project'], + messageId: $schedule['resourceId'], + )); $dbForPlatform->deleteDocument( 'schedules', diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 4d04a3c92c..af3d145f85 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -5,8 +5,9 @@ namespace Appwrite\Platform\Workers; use Appwrite\Certificates\Adapter as CertificatesAdapter; use Appwrite\Event\Event; use Appwrite\Event\Func; -use Appwrite\Event\Mail; +use Appwrite\Event\Message\Mail as MailMessage; use Appwrite\Event\Publisher\Certificate; +use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Event\Realtime; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception as AppwriteException; @@ -50,7 +51,7 @@ class Certificates extends Action ->desc('Certificates worker') ->inject('message') ->inject('dbForPlatform') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('queueForEvents') ->inject('queueForWebhooks') ->inject('queueForFunctions') @@ -66,7 +67,7 @@ class Certificates extends Action /** * @param Message $message * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param Event $queueForEvents * @param Webhook $queueForWebhooks * @param Func $queueForFunctions @@ -83,7 +84,7 @@ class Certificates extends Action public function action( Message $message, Database $dbForPlatform, - Mail $queueForMails, + MailPublisher $publisherForMails, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, @@ -116,7 +117,7 @@ class Certificates extends Action break; case \Appwrite\Event\Certificate::ACTION_GENERATION: - $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain); + $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $publisherForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain); break; default: @@ -209,7 +210,7 @@ class Certificates extends Action * @param Domain $domain * @param ?string $domainType * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param Event $queueForEvents * @param Webhook $queueForWebhooks * @param Func $queueForFunctions @@ -233,7 +234,7 @@ class Certificates extends Action Domain $domain, ?string $domainType, Database $dbForPlatform, - Mail $queueForMails, + MailPublisher $publisherForMails, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, @@ -358,7 +359,7 @@ class Certificates extends Action $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED); // Send email to security email - $this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails, $plan); + $this->notifyError($domain->get(), $e->getMessage(), $attempts, $publisherForMails, $plan); throw $e; } finally { @@ -524,12 +525,12 @@ class Certificates extends Action * @param string $domain Domain that caused the error * @param string $errorMessage Verbose error message * @param int $attempt How many times it failed already - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param array $plan * @return void * @throws Exception */ - private function notifyError(string $domain, string $errorMessage, int $attempt, Mail $queueForMails, array $plan): void + private function notifyError(string $domain, string $errorMessage, int $attempt, MailPublisher $publisherForMails, array $plan): void { // Log error into console Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage); @@ -560,14 +561,14 @@ class Certificates extends Action $subject = $locale->getText("emails.certificate.subject"); $preview = $locale->getText("emails.certificate.preview"); - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setName('Appwrite Administrator') - ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl') - ->setVariables($emailVariables) - ->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'))) - ->trigger(); + $publisherForMails->enqueue(new MailMessage( + recipient: System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')), + name: 'Appwrite Administrator', + subject: $subject, + bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl', + body: $body, + preview: $preview, + variables: $emailVariables, + )); } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index b6c295b3bb..1d3f2f4622 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -3,9 +3,10 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; -use Appwrite\Event\Mail; +use Appwrite\Event\Message\Mail as MailMessage; use Appwrite\Event\Message\Migration; use Appwrite\Event\Message\Usage as UsageMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Event\Realtime; use Appwrite\Extend\Exception; @@ -102,7 +103,7 @@ class Migrations extends Action ->inject('queueForRealtime') ->inject('deviceForMigrations') ->inject('deviceForFiles') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('usage') ->inject('publisherForUsage') ->inject('plan') @@ -124,7 +125,7 @@ class Migrations extends Action Realtime $queueForRealtime, Device $deviceForMigrations, Device $deviceForFiles, - Mail $queueForMails, + MailPublisher $publisherForMails, Context $usage, UsagePublisher $publisherForUsage, array $plan, @@ -163,7 +164,7 @@ class Migrations extends Action $this->processMigration( $migration, $queueForRealtime, - $queueForMails, + $publisherForMails, $usage, $publisherForUsage, $platform, @@ -426,7 +427,7 @@ class Migrations extends Action protected function processMigration( Document $migration, Realtime $queueForRealtime, - Mail $queueForMails, + MailPublisher $publisherForMails, Context $usage, UsagePublisher $publisherForUsage, array $platform, @@ -630,7 +631,7 @@ class Migrations extends Action } $destination_type = $migration->getAttribute('destination'); if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) { - $this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization); + $this->handleDataExportComplete($project, $migration, $publisherForMails, $queueForRealtime, $platform, $authorization); } } finally { $source?->cleanup(); @@ -657,7 +658,7 @@ class Migrations extends Action * * @param Document $project * @param Document $migration - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param Realtime $queueForRealtime * @param array $platform * @param Authorization $authorization @@ -666,7 +667,7 @@ class Migrations extends Action protected function handleDataExportComplete( Document $project, Document $migration, - Mail $queueForMails, + MailPublisher $publisherForMails, Realtime $queueForRealtime, array $platform, Authorization $authorization, @@ -718,7 +719,7 @@ class Migrations extends Action project: $project, user: $user, options: $options, - queueForMails: $queueForMails, + publisherForMails: $publisherForMails, platform: $platform, exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV', sizeMB: $sizeMB @@ -781,7 +782,7 @@ class Migrations extends Action project: $project, user: $user, options: $options, - queueForMails: $queueForMails, + publisherForMails: $publisherForMails, platform: $platform, exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV', downloadUrl: $downloadUrl @@ -795,7 +796,7 @@ class Migrations extends Action * @param Document $project * @param Document $user The user who triggered the operation * @param array $options Migration options - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param array $platform * @param string $downloadUrl Download URL for successful exports * @param float $sizeMB File size in MB for failed exports @@ -807,7 +808,7 @@ class Migrations extends Action Document $project, Document $user, array $options, - Mail $queueForMails, + MailPublisher $publisherForMails, array $platform, string $exportType = 'CSV', string $downloadUrl = '', @@ -877,17 +878,18 @@ class Migrations extends Action 'type' => $exportType, ]; - $queueForMails - ->setProject($project) - ->setSubject($subject) - ->setPreview($preview) - ->setBody($emailBody) - ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl') - ->setVariables($emailVariables) - ->setName($user->getAttribute('name', $user->getAttribute('email'))) - ->setRecipient($user->getAttribute('email')) - ->setSenderName($platform['emailSenderName']) - ->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $user->getAttribute('email'), + name: $user->getAttribute('name', $user->getAttribute('email')), + subject: $subject, + bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl', + body: $emailBody, + preview: $preview, + variables: $emailVariables, + customMailOptions: ['senderName' => $platform['emailSenderName']], + platform: $platform, + )); Console::info("CSV export {$emailType} notification email sent to " . $user->getAttribute('email')); } diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index a7f4595966..973e487de5 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -2,8 +2,9 @@ namespace Appwrite\Platform\Workers; -use Appwrite\Event\Mail; +use Appwrite\Event\Message\Mail as MailMessage; use Appwrite\Event\Message\Usage as UsageMessage; +use Appwrite\Event\Publisher\Mail as MailPublisher; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Template\Template; use Appwrite\Usage\Context as UsageContext; @@ -36,7 +37,7 @@ class Webhooks extends Action ->inject('message') ->inject('project') ->inject('dbForPlatform') - ->inject('queueForMails') + ->inject('publisherForMails') ->inject('publisherForUsage') ->inject('log') ->inject('plan') @@ -47,14 +48,14 @@ class Webhooks extends Action * @param Message $message * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param UsagePublisher $publisherForUsage * @param Log $log * @param array $plan * @return void * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void + public function action(Message $message, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void { $this->errors = []; $payload = $message->getPayload(); @@ -73,7 +74,7 @@ class Webhooks extends Action foreach ($project->getAttribute('webhooks', []) as $webhook) { if (array_intersect($webhook->getAttribute('events', []), $events)) { - $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $publisherForUsage, $plan); + $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $publisherForMails, $publisherForUsage, $plan); } } @@ -89,11 +90,11 @@ class Webhooks extends Action * @param Document $user * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param array $plan * @return void */ - private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, array $plan): void + private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, array $plan): void { if ($webhook->getAttribute('enabled') !== true) { return; @@ -171,7 +172,7 @@ class Webhooks extends Action if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) { $webhook->setAttribute('enabled', false); $updatePayload['enabled'] = false; - $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan); + $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $publisherForMails, $plan); } $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload)); @@ -203,11 +204,11 @@ class Webhooks extends Action * @param Document $webhook * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param MailPublisher $publisherForMails * @param array $plan * @return void */ - public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails, array $plan): void + public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, array $plan): void { $memberships = $dbForPlatform->find('memberships', [ Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]), @@ -251,18 +252,16 @@ class Webhooks extends Action ->setParam('{{message}}', $template->render()) ->setParam('{{year}}', date("Y")); - $queueForMails - ->setProject($project) - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body->render()); - foreach ($users as $user) { - $queueForMails - ->setVariables(['user' => $user->getAttribute('name', '')]) - ->setName($user->getAttribute('name', '')) - ->setRecipient($user->getAttribute('email')) - ->trigger(); + $publisherForMails->enqueue(new MailMessage( + project: $project, + recipient: $user->getAttribute('email'), + name: $user->getAttribute('name', ''), + subject: $subject, + body: $body->render(), + preview: $preview, + variables: ['user' => $user->getAttribute('name', '')], + )); } } } From 18ec96f12417d70dc0273c439ea100e27c9cdcf9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 8 May 2026 14:41:58 +0530 Subject: [PATCH 02/37] Address Greptile feedback for queue publishers --- .../Modules/Teams/Http/Memberships/Create.php | 4 +++- src/Appwrite/Platform/Tasks/ScheduleMessages.php | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index d16a71780a..5500a56cbc 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -95,12 +95,13 @@ class Create extends Action ->inject('timelimit') ->inject('usage') ->inject('plan') + ->inject('platform') ->inject('proofForPassword') ->inject('proofForToken') ->callback($this->action(...)); } - public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken) + public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, array $platform, Password $proofForPassword, Token $proofForToken) { $isAppUser = $user->isApp($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); @@ -417,6 +418,7 @@ class Create extends Action preview: $preview, smtp: $smtpConfig, variables: $emailVariables, + platform: $platform, )); } elseif (! empty($phone)) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 23068fcb9d..634fb26dc2 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -14,6 +14,8 @@ class ScheduleMessages extends ScheduleBase public const UPDATE_TIMER = 3; // seconds public const ENQUEUE_TIMER = 4; // seconds + private ?MessagingPublisher $publisherForMessaging = null; + public static function getName(): string { return 'schedule-messages'; @@ -31,6 +33,11 @@ class ScheduleMessages extends ScheduleBase protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void { + $publisherForMessaging = $this->publisherForMessaging ??= new MessagingPublisher( + $this->publisherMessaging, + new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME)) + ); + foreach ($this->schedules as $schedule) { if (!$schedule['active']) { continue; @@ -43,14 +50,9 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $scheduledAt, $dbForPlatform) { + \go(function () use ($schedule, $scheduledAt, $dbForPlatform, $publisherForMessaging) { $this->updateProjectAccess($schedule['project'], $dbForPlatform); - $publisherForMessaging = new MessagingPublisher( - $this->publisherMessaging, - new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME)) - ); - $publisherForMessaging->enqueue(new MessagingMessage( type: MESSAGE_SEND_TYPE_EXTERNAL, project: $schedule['project'], From fe5e5b889131f43cf6ef63d6024c945fcadecd1f Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 10:17:55 +0200 Subject: [PATCH 03/37] refactor: enhance execution log checks in SitesCustomServerTest --- .../Services/Sites/SitesCustomServerTest.php | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 2beae74d3e..4de17e4462 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2633,6 +2633,7 @@ class SitesCustomServerTest extends Scope // Poll for execution logs to be written (async) // Filter by requestPath to avoid picking up screenshot worker executions + // Wait for both the execution entry AND its logs field to be populated $logs = null; $timeout = 120; $start = \time(); @@ -2642,12 +2643,13 @@ class SitesCustomServerTest extends Scope Query::equal('requestPath', ['/logs-inline'])->toString(), Query::limit(1)->toString(), ]); - if (!empty($logs['body']['executions'])) { + if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) { break; } \usleep(500000); } $this->assertNotEmpty($logs['body']['executions'], 'Execution logs were not available within timeout'); + $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Execution logs content was not populated within timeout'); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']); $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); @@ -2681,11 +2683,20 @@ class SitesCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Action logs printed.", $response['body']); - $logs = $this->listLogs($siteId, [ - Query::orderDesc('$createdAt')->toString(), - Query::equal('requestPath', ['/logs-action'])->toString(), - Query::limit(1)->toString(), - ]); + $logs = null; + $start = \time(); + while (\time() - $start < $timeout) { + $logs = $this->listLogs($siteId, [ + Query::orderDesc('$createdAt')->toString(), + Query::equal('requestPath', ['/logs-action'])->toString(), + Query::limit(1)->toString(), + ]); + if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) { + break; + } + \usleep(500000); + } + $this->assertNotEmpty($logs['body']['executions'], 'Action execution logs were not available within timeout'); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']); $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); From 702a8a83a04331911faf51f9816622c477cbbf51 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 10:30:08 +0200 Subject: [PATCH 04/37] test: add assertion for action execution logs content in SitesCustomServerTest --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 4de17e4462..a32b990b9e 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2697,6 +2697,7 @@ class SitesCustomServerTest extends Scope \usleep(500000); } $this->assertNotEmpty($logs['body']['executions'], 'Action execution logs were not available within timeout'); + $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Action execution logs content was not populated within timeout'); $this->assertEquals(200, $logs['headers']['status-code']); $this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']); $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); From 0fb2e208ab376343b34bda60a36d4ec0941d4a79 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 10:47:22 +0200 Subject: [PATCH 05/37] chore: add spot=false label to the runs-on based runners --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb58f7f8cd..cf38e04c8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -445,19 +445,19 @@ jobs: ] include: - service: Databases - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: Sites - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - service: Functions - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - service: Avatars - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - service: Realtime - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - service: TablesDB - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g + runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: Migrations From d9494820944ca576d9dae0a7f2ed3696d8d136ce Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 11:10:33 +0200 Subject: [PATCH 06/37] chore: set bigger size on database related e2e --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf38e04c8a..0e13c23bf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -445,7 +445,7 @@ jobs: ] include: - service: Databases - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false + runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/family=c7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: Sites From 75d4e931bce8fd4187e020e5b9467df8384d425c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 11:17:41 +0200 Subject: [PATCH 07/37] Project response model rework beginning --- .../Modules/Projects/Http/Projects/Create.php | 20 +- .../Utopia/Response/Model/Project.php | 236 ++++-------------- 2 files changed, 43 insertions(+), 213 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index 363c99dc1f..67cb5ab0fd 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -72,15 +72,6 @@ class Create extends Action ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('teamId', '', new UID(), 'Team unique ID.') ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) - ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) - ->param('logo', '', new Text(1024), 'Project logo.', true) - ->param('url', '', new URL(), 'Project URL.', true) - ->param('legalName', '', new Text(256), 'Project legal Name. Max length: 256 chars.', true) - ->param('legalCountry', '', new Text(256), 'Project legal Country. Max length: 256 chars.', true) - ->param('legalState', '', new Text(256), 'Project legal State. Max length: 256 chars.', true) - ->param('legalCity', '', new Text(256), 'Project legal City. Max length: 256 chars.', true) - ->param('legalAddress', '', new Text(256), 'Project legal Address. Max length: 256 chars.', true) - ->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true) ->inject('request') ->inject('response') ->inject('dbForPlatform') @@ -90,7 +81,7 @@ class Create extends Action ->callback($this->action(...)); } - public function action(string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) + public function action(string $projectId, string $name, string $teamId, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) { $team = $dbForPlatform->getDocument('teams', $teamId); @@ -175,16 +166,7 @@ class Create extends Action 'teamInternalId' => $team->getSequence(), 'teamId' => $team->getId(), 'region' => $region, - 'description' => $description, - 'logo' => $logo, - 'url' => $url, 'version' => APP_VERSION_STABLE, - 'legalName' => $legalName, - 'legalCountry' => $legalCountry, - 'legalState' => $legalState, - 'legalCity' => $legalCity, - 'legalAddress' => $legalAddress, - 'legalTaxId' => ID::custom($legalTaxId), 'services' => new \stdClass(), 'platforms' => null, 'oAuthProviders' => [], diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 36be3b751f..132b228ed6 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -12,6 +12,7 @@ class Project extends Model public function __construct() { $this + // Basic project information ->addRule('$id', [ 'type' => self::TYPE_STRING, 'description' => 'Project ID.', @@ -36,203 +37,14 @@ class Project extends Model 'default' => '', 'example' => 'New Project', ]) - ->addRule('description', [ - 'type' => self::TYPE_STRING, - 'description' => 'Project description.', - 'default' => '', - 'example' => 'This is a new project.', - ]) ->addRule('teamId', [ 'type' => self::TYPE_STRING, 'description' => 'Project team ID.', 'default' => '', 'example' => '1592981250', ]) - ->addRule('logo', [ - 'type' => self::TYPE_STRING, - 'description' => 'Project logo file ID.', - 'default' => '', - 'example' => '5f5c451b403cb', - ]) - ->addRule('url', [ - 'type' => self::TYPE_STRING, - 'description' => 'Project website URL.', - 'default' => '', - 'example' => '5f5c451b403cb', - ]) - ->addRule('legalName', [ - 'type' => self::TYPE_STRING, - 'description' => 'Company legal name.', - 'default' => '', - 'example' => 'Company LTD.', - ]) - ->addRule('legalCountry', [ - 'type' => self::TYPE_STRING, - 'description' => 'Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format.', - 'default' => '', - 'example' => 'US', - ]) - ->addRule('legalState', [ - 'type' => self::TYPE_STRING, - 'description' => 'State name.', - 'default' => '', - 'example' => 'New York', - ]) - ->addRule('legalCity', [ - 'type' => self::TYPE_STRING, - 'description' => 'City name.', - 'default' => '', - 'example' => 'New York City.', - ]) - ->addRule('legalAddress', [ - 'type' => self::TYPE_STRING, - 'description' => 'Company Address.', - 'default' => '', - 'example' => '620 Eighth Avenue, New York, NY 10018', - ]) - ->addRule('legalTaxId', [ - 'type' => self::TYPE_STRING, - 'description' => 'Company Tax ID.', - 'default' => '', - 'example' => '131102020', - ]) - ->addRule('authDuration', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Session duration in seconds.', - 'default' => TOKEN_EXPIRATION_LOGIN_LONG, - 'example' => 60, - ]) - ->addRule('authLimit', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Max users allowed. 0 is unlimited.', - 'default' => 0, - 'example' => 100, - ]) - ->addRule('authSessionsLimit', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Max sessions allowed per user. 100 maximum.', - 'default' => 10, - 'example' => 10, - ]) - ->addRule('authPasswordHistory', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Max allowed passwords in the history list per user. Max passwords limit allowed in history is 20. Use 0 for disabling password history.', - 'default' => 0, - 'example' => 5, - ]) - ->addRule('authPasswordDictionary', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to check user\'s password against most commonly used passwords.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authPersonalDataCheck', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to check the user password for similarity with their personal data.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authDisposableEmails', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authCanonicalEmails', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to require canonical email addresses during signup and email updates.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authFreeEmails', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to disallow free email addresses during signup and email updates.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMockNumbers', [ - 'type' => Response::MODEL_MOCK_NUMBER, - 'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).', - 'default' => [], - 'array' => true, - 'example' => [new \stdClass()], - ]) - ->addRule('authSessionAlerts', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to send session alert emails to users.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMembershipsUserName', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to show user names in the teams membership response.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMembershipsUserEmail', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to show user emails in the teams membership response.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMembershipsMfa', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to show user MFA status in the teams membership response.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMembershipsUserId', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to show user IDs in the teams membership response.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authMembershipsUserPhone', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not to show user phone numbers in the teams membership response.', - 'default' => false, - 'example' => true, - ]) - ->addRule('authInvalidateSessions', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Whether or not all existing sessions should be invalidated on password change', - 'default' => false, - 'example' => true, - ]) - ->addRule('oAuthProviders', [ - 'type' => Response::MODEL_AUTH_PROVIDER, - 'description' => 'List of Auth Providers.', - 'default' => [], - 'example' => [new \stdClass()], - 'array' => true, - ]) - ->addRule('platforms', [ - 'type' => [ - Response::MODEL_PLATFORM_WEB, - Response::MODEL_PLATFORM_APPLE, - Response::MODEL_PLATFORM_ANDROID, - Response::MODEL_PLATFORM_WINDOWS, - Response::MODEL_PLATFORM_LINUX, - ], - 'description' => 'List of Platforms.', - 'default' => [], - 'example' => new \stdClass(), - 'array' => true, - ]) - ->addRule('webhooks', [ - 'type' => Response::MODEL_WEBHOOK, - 'description' => 'List of Webhooks.', - 'default' => [], - 'example' => new \stdClass(), - 'array' => true, - ]) - ->addRule('keys', [ - 'type' => Response::MODEL_KEY, - 'description' => 'List of API Keys.', - 'default' => [], - 'example' => new \stdClass(), - 'array' => true, - ]) + + // Resource: Dev Keys ->addRule('devKeys', [ 'type' => Response::MODEL_DEV_KEY, 'description' => 'List of dev keys.', @@ -240,6 +52,8 @@ class Project extends Model 'example' => new \stdClass(), 'array' => true, ]) + + // Resource: SMTP ->addRule('smtpEnabled', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'Status for custom SMTP', @@ -301,6 +115,8 @@ class Project extends Model 'default' => '', 'example' => 'tls', ]) + + // Resource: Ping ->addRule('pingCount', [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of times the ping was received for this project.', @@ -313,6 +129,8 @@ class Project extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + + // Resource: Labels ->addRule('labels', [ 'type' => self::TYPE_STRING, 'description' => 'Labels for the project.', @@ -320,17 +138,45 @@ class Project extends Model 'example' => ['vip'], 'array' => true, ]) + + // Resource: Billing ->addRule('status', [ 'type' => self::TYPE_STRING, 'description' => 'Project status.', 'default' => 'active', 'example' => 'active', ]) + + // Resource: Auth methods + ->addRule('authMethods', [ + 'type' => Response::MODEL_PROJECT_AUTH_METHOD, + 'description' => 'List of auth methods.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true, + ]) + + // Resource: Services + ->addRule('services', [ + 'type' => Response::MODEL_PROJECT_SERVICE, + 'description' => 'List of services.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true, + ]) + + // Resource: Protocols + ->addRule('protocols', [ + 'type' => Response::MODEL_PROJECT_PROTOCOL, + 'description' => 'List of protocols.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true, + ]) ; - $services = Config::getParam('services', []); + // Resource: Auth methods $auth = Config::getParam('auth', []); - foreach ($auth as $index => $method) { $name = $method['name'] ?? ''; $key = $method['key'] ?? ''; @@ -345,6 +191,8 @@ class Project extends Model ; } + // Resource: Services + $services = Config::getParam('services', []); foreach ($services as $service) { if (!$service['optional']) { continue; @@ -363,8 +211,8 @@ class Project extends Model ; } + // Resource: Protocols $apis = Config::getParam('protocols', []); - foreach ($apis as $api) { $name = $api['name'] ?? ''; $key = $api['key'] ?? ''; From 443ebac71c4fc74bf9a4d64cd0746c46430fdd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 11:56:22 +0200 Subject: [PATCH 08/37] Finish project response model --- app/init/models.php | 6 + src/Appwrite/Utopia/Response.php | 3 + .../Utopia/Response/Model/Project.php | 139 ++++-------------- .../Response/Model/ProjectAuthMethod.php | 47 ++++++ .../Utopia/Response/Model/ProjectProtocol.php | 47 ++++++ .../Utopia/Response/Model/ProjectService.php | 47 ++++++ 6 files changed, 175 insertions(+), 114 deletions(-) create mode 100644 src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php create mode 100644 src/Appwrite/Utopia/Response/Model/ProjectProtocol.php create mode 100644 src/Appwrite/Utopia/Response/Model/ProjectService.php diff --git a/app/init/models.php b/app/init/models.php index 8276e2561e..f983c43f0a 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -173,6 +173,9 @@ use Appwrite\Utopia\Response\Model\PolicySessionLimit; use Appwrite\Utopia\Response\Model\PolicyUserLimit; use Appwrite\Utopia\Response\Model\Preferences; use Appwrite\Utopia\Response\Model\Project; +use Appwrite\Utopia\Response\Model\ProjectAuthMethod; +use Appwrite\Utopia\Response\Model\ProjectProtocol; +use Appwrite\Utopia\Response\Model\ProjectService; use Appwrite\Utopia\Response\Model\Provider; use Appwrite\Utopia\Response\Model\ProviderRepository; use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework; @@ -397,6 +400,9 @@ Response::setModel(new FrameworkAdapter()); Response::setModel(new Deployment()); Response::setModel(new Execution()); Response::setModel(new Project()); +Response::setModel(new ProjectAuthMethod()); +Response::setModel(new ProjectService()); +Response::setModel(new ProjectProtocol()); Response::setModel(new Webhook()); Response::setModel(new Key()); Response::setModel(new EphemeralKey()); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index e02e70b3d8..01f86cb5e8 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -249,6 +249,9 @@ class Response extends SwooleResponse // Project public const MODEL_PROJECT = 'project'; public const MODEL_PROJECT_LIST = 'projectList'; + public const MODEL_PROJECT_AUTH_METHOD = 'projectAuthMethod'; + public const MODEL_PROJECT_SERVICE = 'projectService'; + public const MODEL_PROJECT_PROTOCOL = 'projectProtocol'; public const MODEL_WEBHOOK = 'webhook'; public const MODEL_WEBHOOK_LIST = 'webhookList'; public const MODEL_KEY = 'key'; diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 132b228ed6..b25fdc51e9 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -174,58 +174,6 @@ class Project extends Model 'array' => true, ]) ; - - // Resource: Auth methods - $auth = Config::getParam('auth', []); - foreach ($auth as $index => $method) { - $name = $method['name'] ?? ''; - $key = $method['key'] ?? ''; - - $this - ->addRule('auth' . ucfirst($key), [ - 'type' => self::TYPE_BOOLEAN, - 'description' => $name . ' auth method status', - 'example' => true, - 'default' => true, - ]) - ; - } - - // Resource: Services - $services = Config::getParam('services', []); - foreach ($services as $service) { - if (!$service['optional']) { - continue; - } - - $name = $service['name'] ?? ''; - $key = $service['key'] ?? ''; - - $this - ->addRule('serviceStatusFor' . ucfirst($key), [ - 'type' => self::TYPE_BOOLEAN, - 'description' => $name . ' service status', - 'example' => true, - 'default' => true, - ]) - ; - } - - // Resource: Protocols - $apis = Config::getParam('protocols', []); - foreach ($apis as $api) { - $name = $api['name'] ?? ''; - $key = $api['key'] ?? ''; - - $this - ->addRule('protocolStatusFor' . ucfirst($key), [ - 'type' => self::TYPE_BOOLEAN, - 'description' => $name . ' protocol status', - 'example' => true, - 'default' => true, - ]) - ; - } } /** @@ -259,7 +207,6 @@ class Project extends Model $this->expandServiceFields($document); $this->expandApiFields($document); $this->expandAuthFields($document); - $this->expandOAuthProviders($document); return $document; } @@ -270,8 +217,8 @@ class Project extends Model return; } - // SMTP $smtp = $document->getAttribute('smtp', []); + $document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false); $document->setAttribute('smtpSenderEmail', $smtp['senderEmail'] ?? ''); $document->setAttribute('smtpSenderName', $smtp['senderName'] ?? ''); @@ -291,16 +238,20 @@ class Project extends Model } $values = $document->getAttribute('services', []); - $services = Config::getParam('services', []); + $services = []; - foreach ($services as $service) { + foreach (Config::getParam('services', []) as $id => $service) { if (!$service['optional']) { continue; } - $key = $service['key'] ?? ''; - $value = $values[$key] ?? true; - $document->setAttribute('serviceStatusFor' . ucfirst($key), $value); + + $services[] = new Document([ + '$id' => $id, + 'enabled' => $values[$service['key']] ?? true, + ]); } + + $document->setAttribute('services', $services); } private function expandApiFields(Document $document): void @@ -310,13 +261,16 @@ class Project extends Model } $values = $document->getAttribute('apis', []); - $apis = Config::getParam('protocols', []); + $protocols = []; - foreach ($apis as $api) { - $key = $api['key'] ?? ''; - $value = $values[$key] ?? true; - $document->setAttribute('protocolStatusFor' . ucfirst($key), $value); + foreach (Config::getParam('protocols', []) as $id => $api) { + $protocols[] = new Document([ + '$id' => $id, + 'enabled' => $values[$api['key']] ?? true, + ]); } + + $document->setAttribute('protocols', $protocols); } private function expandAuthFields(Document $document): void @@ -326,58 +280,15 @@ class Project extends Model } $authValues = $document->getAttribute('auths', []); - $auth = Config::getParam('auth', []); + $authMethods = []; - $document->setAttribute('authLimit', $authValues['limit'] ?? 0); - $document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG); - $document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? 0); - $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0); - $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false); - $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false); - $document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false); - $document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false); - $document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false); - $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []); - $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false); - $document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? false); - $document->setAttribute('authMembershipsUserEmail', $authValues['membershipsUserEmail'] ?? false); - $document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? false); - $document->setAttribute('authMembershipsUserId', $authValues['membershipsUserId'] ?? false); - $document->setAttribute('authMembershipsUserPhone', $authValues['membershipsUserPhone'] ?? false); - $document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false); - - foreach ($auth as $method) { - $key = $method['key']; - $value = $authValues[$key] ?? true; - $document->setAttribute('auth' . ucfirst($key), $value); - } - } - - private function expandOAuthProviders(Document $document): void - { - if (!$document->isSet('oAuthProviders')) { - return; - } - - $providers = Config::getParam('oAuthProviders', []); - $providerValues = $document->getAttribute('oAuthProviders', []); - $projectProviders = []; - - foreach ($providers as $key => $provider) { - if (!$provider['enabled']) { - // Disabled by Appwrite configuration, exclude from response - continue; - } - - $projectProviders[] = new Document([ - 'key' => $key, - 'name' => $provider['name'] ?? '', - 'appId' => $providerValues[$key . 'Appid'] ?? '', - 'secret' => '', // Write-only: never expose the stored value - 'enabled' => $providerValues[$key . 'Enabled'] ?? false, + foreach (Config::getParam('auth', []) as $id => $method) { + $authMethods[] = new Document([ + '$id' => $id, + 'enabled' => $authValues[$method['key']] ?? true ]); } - - $document->setAttribute('oAuthProviders', $projectProviders); + + $document->setAttribute('authMethods', $authMethods); } } diff --git a/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php new file mode 100644 index 0000000000..b4e9820c54 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php @@ -0,0 +1,47 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Auth method ID.', + 'default' => '', + 'example' => 'email-password', + ]) + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Auth method status.', + 'example' => false, + 'default' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ProjectAuthMethod'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_PROJECT_AUTH_METHOD; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php new file mode 100644 index 0000000000..51addc5366 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php @@ -0,0 +1,47 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Protocol ID.', + 'default' => '', + 'example' => 'email-password', + ]) + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Protocol status.', + 'example' => false, + 'default' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ProjectProtocol'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_PROJECT_PROTOCOL; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ProjectService.php b/src/Appwrite/Utopia/Response/Model/ProjectService.php new file mode 100644 index 0000000000..654936ffb4 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ProjectService.php @@ -0,0 +1,47 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Service ID.', + 'default' => '', + 'example' => 'email-password', + ]) + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Service status.', + 'example' => false, + 'default' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'ProjectService'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_PROJECT_SERVICE; + } +} From beab655ebab756f3f4e22bb981068c9881f0bcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:03:48 +0200 Subject: [PATCH 09/37] Code quality details --- .../Modules/Projects/Http/Projects/Create.php | 1 - .../Utopia/Response/Model/Project.php | 22 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index 67cb5ab0fd..d2c92fc65c 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -28,7 +28,6 @@ use Utopia\Pools\Group; use Utopia\System\System; use Utopia\Validator; use Utopia\Validator\Text; -use Utopia\Validator\URL; use Utopia\Validator\WhiteList; class Create extends Action diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index b25fdc51e9..f83ed8e663 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -204,9 +204,9 @@ class Project extends Model public function filter(Document $document): Document { $this->expandSmtpFields($document); - $this->expandServiceFields($document); - $this->expandApiFields($document); - $this->expandAuthFields($document); + $this->expandServices($document); + $this->expandProtocols($document); + $this->expandAuthMethods($document); return $document; } @@ -218,7 +218,7 @@ class Project extends Model } $smtp = $document->getAttribute('smtp', []); - + $document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false); $document->setAttribute('smtpSenderEmail', $smtp['senderEmail'] ?? ''); $document->setAttribute('smtpSenderName', $smtp['senderName'] ?? ''); @@ -231,7 +231,7 @@ class Project extends Model $document->setAttribute('smtpSecure', $smtp['secure'] ?? ''); } - private function expandServiceFields(Document $document): void + private function expandServices(Document $document): void { if (!$document->isSet('services')) { return; @@ -250,11 +250,11 @@ class Project extends Model 'enabled' => $values[$service['key']] ?? true, ]); } - + $document->setAttribute('services', $services); } - private function expandApiFields(Document $document): void + private function expandProtocols(Document $document): void { if (!$document->isSet('apis')) { return; @@ -273,22 +273,22 @@ class Project extends Model $document->setAttribute('protocols', $protocols); } - private function expandAuthFields(Document $document): void + private function expandAuthMethods(Document $document): void { if (!$document->isSet('auths')) { return; } - $authValues = $document->getAttribute('auths', []); + $values = $document->getAttribute('auths', []); $authMethods = []; foreach (Config::getParam('auth', []) as $id => $method) { $authMethods[] = new Document([ '$id' => $id, - 'enabled' => $authValues[$method['key']] ?? true + 'enabled' => $values[$method['key']] ?? true ]); } - + $document->setAttribute('authMethods', $authMethods); } } From fd893a8b104a64db57bceee129819e5e55d03ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:10:38 +0200 Subject: [PATCH 10/37] Migrate project GET endpoint --- app/controllers/api/projects.php | 34 ---------- .../Modules/Project/Http/Project/Get.php | 64 +++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + 3 files changed, 66 insertions(+), 34 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/Get.php diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 494aa11150..8ab30fac99 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -2,9 +2,6 @@ use Appwrite\Auth\Validator\MockNumber; use Appwrite\Extend\Exception; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Database\Database; @@ -27,37 +24,6 @@ Http::init() } }); -Http::get('/v1/projects/:projectId') - ->desc('Get project') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'get', - description: '/docs/references/projects/get.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $response->dynamic($project, Response::MODEL_PROJECT); - }); - // Backwards compatibility Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php new file mode 100644 index 0000000000..ff1754ec2b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/project') + ->httpAlias('/v1/projects/:projectId') + ->desc('Get project') + ->groups(['api', 'project']) + ->label('scope', 'project.read') + ->label('sdk', new Method( + namespace: 'project', + group: null, + name: 'get', + description: <<inject('response') + ->inject('project') + ->callback($this->action(...)); + } + + public function action( + Response $response, + Document $project, + ) { + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $response->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 609de96530..d36d0e9005 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Project\Services; use Appwrite\Platform\Modules\Project\Http\Init; use Appwrite\Platform\Modules\Project\Http\Project\AuthMethods\Update as UpdateAuthMethod; use Appwrite\Platform\Modules\Project\Http\Project\Delete as DeleteProject; +use Appwrite\Platform\Modules\Project\Http\Project\Get as GetProject; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey; use Appwrite\Platform\Modules\Project\Http\Project\Keys\Ephemeral\Create as CreateEphemeralKey; @@ -110,6 +111,7 @@ class Http extends Service // Project $this->addAction(DeleteProject::getName(), new DeleteProject()); + $this->addAction(GetProject::getName(), new GetProject()); $this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels()); $this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol()); $this->addAction(UpdateProjectService::getName(), new UpdateProjectService()); From 14493f7ea411d139ce2e9dbfa821f39f0d0ef484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:13:36 +0200 Subject: [PATCH 11/37] PR review fixes --- src/Appwrite/Utopia/Response/Model/Project.php | 12 ------------ .../Utopia/Response/Model/ProjectProtocol.php | 2 +- .../Utopia/Response/Model/ProjectService.php | 2 +- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index f83ed8e663..9998c35f2e 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -233,10 +233,6 @@ class Project extends Model private function expandServices(Document $document): void { - if (!$document->isSet('services')) { - return; - } - $values = $document->getAttribute('services', []); $services = []; @@ -256,10 +252,6 @@ class Project extends Model private function expandProtocols(Document $document): void { - if (!$document->isSet('apis')) { - return; - } - $values = $document->getAttribute('apis', []); $protocols = []; @@ -275,10 +267,6 @@ class Project extends Model private function expandAuthMethods(Document $document): void { - if (!$document->isSet('auths')) { - return; - } - $values = $document->getAttribute('auths', []); $authMethods = []; diff --git a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php index 51addc5366..5f56a3089a 100644 --- a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php +++ b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php @@ -14,7 +14,7 @@ class ProjectProtocol extends Model 'type' => self::TYPE_STRING, 'description' => 'Protocol ID.', 'default' => '', - 'example' => 'email-password', + 'example' => 'graphql', ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, diff --git a/src/Appwrite/Utopia/Response/Model/ProjectService.php b/src/Appwrite/Utopia/Response/Model/ProjectService.php index 654936ffb4..792a34f409 100644 --- a/src/Appwrite/Utopia/Response/Model/ProjectService.php +++ b/src/Appwrite/Utopia/Response/Model/ProjectService.php @@ -14,7 +14,7 @@ class ProjectService extends Model 'type' => self::TYPE_STRING, 'description' => 'Service ID.', 'default' => '', - 'example' => 'email-password', + 'example' => 'sites', ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, From 1f7f1e7b31b64266db82cfb72a32d804f62a3cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:16:04 +0200 Subject: [PATCH 12/37] Fix failing to startup issue --- src/Appwrite/Platform/Modules/Project/Http/Project/Get.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php index ff1754ec2b..197d82ef58 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php @@ -12,7 +12,7 @@ use Utopia\Database\Document; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -class Delete extends Action +class Get extends Action { use HTTP; From 5727d25f32038923f9243a629b64d6fb6da97fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:19:51 +0200 Subject: [PATCH 13/37] Add enum to project response models --- src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php | 4 +++- src/Appwrite/Utopia/Response/Model/ProjectProtocol.php | 4 +++- src/Appwrite/Utopia/Response/Model/ProjectService.php | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php index b4e9820c54..e500d7caa6 100644 --- a/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php +++ b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use Utopia\Config\Config; class ProjectAuthMethod extends Model { @@ -11,10 +12,11 @@ class ProjectAuthMethod extends Model { $this ->addRule('$id', [ - 'type' => self::TYPE_STRING, + 'type' => self::TYPE_ENUM, 'description' => 'Auth method ID.', 'default' => '', 'example' => 'email-password', + 'enum' => \array_keys(Config::getParam('auth', [])), ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, diff --git a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php index 5f56a3089a..fc166be9aa 100644 --- a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php +++ b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use Utopia\Config\Config; class ProjectProtocol extends Model { @@ -11,10 +12,11 @@ class ProjectProtocol extends Model { $this ->addRule('$id', [ - 'type' => self::TYPE_STRING, + 'type' => self::TYPE_ENUM, 'description' => 'Protocol ID.', 'default' => '', 'example' => 'graphql', + 'enum' => \array_keys(Config::getParam('protocols', [])), ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, diff --git a/src/Appwrite/Utopia/Response/Model/ProjectService.php b/src/Appwrite/Utopia/Response/Model/ProjectService.php index 792a34f409..ca481c2946 100644 --- a/src/Appwrite/Utopia/Response/Model/ProjectService.php +++ b/src/Appwrite/Utopia/Response/Model/ProjectService.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use Utopia\Config\Config; class ProjectService extends Model { @@ -11,10 +12,11 @@ class ProjectService extends Model { $this ->addRule('$id', [ - 'type' => self::TYPE_STRING, + 'type' => self::TYPE_ENUM, 'description' => 'Service ID.', 'default' => '', 'example' => 'sites', + 'enum' => \array_keys(\array_filter(Config::getParam('services', []), fn ($element) => $element['optional'])), ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, From 4e36040f475be44187cbfb4f5d7e50ad4dd6110c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:22:17 +0200 Subject: [PATCH 14/37] Add project GET tests --- .../Project/ProjectConsoleClientTest.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/e2e/Services/Project/ProjectConsoleClientTest.php b/tests/e2e/Services/Project/ProjectConsoleClientTest.php index 0ba69c21b6..389e4d800a 100644 --- a/tests/e2e/Services/Project/ProjectConsoleClientTest.php +++ b/tests/e2e/Services/Project/ProjectConsoleClientTest.php @@ -51,6 +51,85 @@ class ProjectConsoleClientTest extends Scope $this->assertSame(404, $getProject['headers']['status-code']); } + public function testGetProject(): void + { + $team = $this->createTeam('Get Project Team'); + $project = $this->createProject($team['body']['$id'], 'Get Project'); + + $response = $this->client->call(Client::METHOD_GET, '/project', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $project['body']['$id'], + ], $this->getHeaders())); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame($project['body']['$id'], $response['body']['$id']); + $this->assertSame('Get Project', $response['body']['name']); + $this->assertSame($team['body']['$id'], $response['body']['teamId']); + $this->assertSame('active', $response['body']['status']); + + // Auth methods + $this->assertIsArray($response['body']['authMethods']); + $this->assertNotEmpty($response['body']['authMethods']); + foreach ($response['body']['authMethods'] as $authMethod) { + $this->assertArrayHasKey('$id', $authMethod); + $this->assertArrayHasKey('enabled', $authMethod); + $this->assertIsBool($authMethod['enabled']); + } + + // Services + $this->assertIsArray($response['body']['services']); + $this->assertNotEmpty($response['body']['services']); + foreach ($response['body']['services'] as $service) { + $this->assertArrayHasKey('$id', $service); + $this->assertArrayHasKey('enabled', $service); + $this->assertIsBool($service['enabled']); + } + + // Protocols + $this->assertIsArray($response['body']['protocols']); + $this->assertNotEmpty($response['body']['protocols']); + foreach ($response['body']['protocols'] as $protocol) { + $this->assertArrayHasKey('$id', $protocol); + $this->assertArrayHasKey('enabled', $protocol); + $this->assertIsBool($protocol['enabled']); + } + + // SMTP defaults + $this->assertFalse($response['body']['smtpEnabled']); + $this->assertSame('', $response['body']['smtpSenderEmail']); + $this->assertSame('', $response['body']['smtpSenderName']); + $this->assertSame('', $response['body']['smtpReplyToEmail']); + $this->assertSame('', $response['body']['smtpReplyToName']); + $this->assertSame('', $response['body']['smtpHost']); + $this->assertSame('', $response['body']['smtpPort']); + $this->assertSame('', $response['body']['smtpUsername']); + $this->assertSame('', $response['body']['smtpPassword']); + $this->assertSame('', $response['body']['smtpSecure']); + + // Other fields + $this->assertIsArray($response['body']['labels']); + $this->assertIsArray($response['body']['devKeys']); + $this->assertSame(0, $response['body']['pingCount']); + + // Ensure old flattened fields are not present + $this->assertArrayNotHasKey('description', $response['body']); + $this->assertArrayNotHasKey('logo', $response['body']); + $this->assertArrayNotHasKey('url', $response['body']); + $this->assertArrayNotHasKey('authDuration', $response['body']); + $this->assertArrayNotHasKey('authLimit', $response['body']); + $this->assertArrayNotHasKey('authSessionsLimit', $response['body']); + $this->assertArrayNotHasKey('authPasswordHistory', $response['body']); + $this->assertArrayNotHasKey('authPasswordDictionary', $response['body']); + $this->assertArrayNotHasKey('authPersonalDataCheck', $response['body']); + $this->assertArrayNotHasKey('authDisposableEmails', $response['body']); + $this->assertArrayNotHasKey('authCanonicalEmails', $response['body']); + $this->assertArrayNotHasKey('authFreeEmails', $response['body']); + $this->assertArrayNotHasKey('oAuthProviders', $response['body']); + $this->assertArrayNotHasKey('platforms', $response['body']); + $this->assertArrayNotHasKey('webhooks', $response['body']); + $this->assertArrayNotHasKey('keys', $response['body']); + } + protected function createTeam(string $name): array { $response = $this->client->call(Client::METHOD_POST, '/teams', $this->getConsoleSessionHeaders(), [ From 83887b09b647bdb416dfe403bf1c8c8c177be73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:47:53 +0200 Subject: [PATCH 15/37] Improve project get tests --- tests/e2e/Services/Project/ProjectConsoleClientTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/Services/Project/ProjectConsoleClientTest.php b/tests/e2e/Services/Project/ProjectConsoleClientTest.php index 389e4d800a..a4c8b73efc 100644 --- a/tests/e2e/Services/Project/ProjectConsoleClientTest.php +++ b/tests/e2e/Services/Project/ProjectConsoleClientTest.php @@ -63,6 +63,8 @@ class ProjectConsoleClientTest extends Scope $this->assertSame(200, $response['headers']['status-code']); $this->assertSame($project['body']['$id'], $response['body']['$id']); + $this->assertNotEmpty($response['body']['$createdAt']); + $this->assertNotEmpty($response['body']['$updatedAt']); $this->assertSame('Get Project', $response['body']['name']); $this->assertSame($team['body']['$id'], $response['body']['teamId']); $this->assertSame('active', $response['body']['status']); @@ -110,6 +112,7 @@ class ProjectConsoleClientTest extends Scope $this->assertIsArray($response['body']['labels']); $this->assertIsArray($response['body']['devKeys']); $this->assertSame(0, $response['body']['pingCount']); + $this->assertSame('', $response['body']['pingedAt']); // Ensure old flattened fields are not present $this->assertArrayNotHasKey('description', $response['body']); From 9fae4c965f744c647976bec31f30c3fa93fe9b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:50:44 +0200 Subject: [PATCH 16/37] Unpin console version to get latest automatically Only during local development --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2ae1bf486a..76f06c672a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -255,7 +255,7 @@ services: appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:7.8.45 + image: appwrite/console:8 restart: unless-stopped networks: - appwrite From 2da400d5b4b7c448a13f6c07fd8cb51b97bbca72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 12:53:01 +0200 Subject: [PATCH 17/37] Bump version to introduce response filter (for project get) --- app/controllers/general.php | 8 ++++++++ app/init/constants.php | 2 +- src/Appwrite/Migration/Migration.php | 1 + src/Appwrite/Utopia/Request/Filters/V26.php | 14 ++++++++++++++ src/Appwrite/Utopia/Response/Filters/V26.php | 14 ++++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/Appwrite/Utopia/Request/Filters/V26.php create mode 100644 src/Appwrite/Utopia/Response/Filters/V26.php diff --git a/app/controllers/general.php b/app/controllers/general.php index bc63d200d7..f80bc3b631 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -29,6 +29,7 @@ use Appwrite\Utopia\Request\Filters\V22 as RequestV22; use Appwrite\Utopia\Request\Filters\V23 as RequestV23; use Appwrite\Utopia\Request\Filters\V24 as RequestV24; use Appwrite\Utopia\Request\Filters\V25 as RequestV25; +use Appwrite\Utopia\Request\Filters\V26 as RequestV26; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -40,6 +41,7 @@ use Appwrite\Utopia\Response\Filters\V22 as ResponseV22; use Appwrite\Utopia\Response\Filters\V23 as ResponseV23; use Appwrite\Utopia\Response\Filters\V24 as ResponseV24; use Appwrite\Utopia\Response\Filters\V25 as ResponseV25; +use Appwrite\Utopia\Response\Filters\V26 as ResponseV26; use Appwrite\Utopia\View; use Executor\Exception\Timeout as ExecutorTimeout; use Executor\Executor; @@ -914,6 +916,9 @@ Http::init() if (version_compare($requestFormat, '1.9.4', '<')) { $request->addFilter(new RequestV25()); } + if (version_compare($requestFormat, '1.9.5', '<')) { + $request->addFilter(new RequestV26()); + } } $localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); @@ -938,6 +943,9 @@ Http::init() */ $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { + if (version_compare($responseFormat, '1.9.5', '<')) { + $response->addFilter(new ResponseV26()); + } if (version_compare($responseFormat, '1.9.4', '<')) { $response->addFilter(new ResponseV25()); } diff --git a/app/init/constants.php b/app/init/constants.php index 8bc9b6a826..17afc35ae9 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -45,7 +45,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4325; -const APP_VERSION_STABLE = '1.9.4'; +const APP_VERSION_STABLE = '1.9.5'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 08e32a9c74..77c62bce96 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -97,6 +97,7 @@ abstract class Migration '1.9.2' => 'V24', '1.9.3' => 'V24', '1.9.4' => 'V24', + '1.9.5' => 'V24', ]; /** diff --git a/src/Appwrite/Utopia/Request/Filters/V26.php b/src/Appwrite/Utopia/Request/Filters/V26.php new file mode 100644 index 0000000000..62406ca519 --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V26.php @@ -0,0 +1,14 @@ + Date: Mon, 11 May 2026 13:12:25 +0200 Subject: [PATCH 18/37] chore: update runner configuration for e2e_service jobs in ci workflow --- .github/workflows/ci.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e13c23bf7..2de36bc42f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -406,7 +406,7 @@ jobs: e2e_service: name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }} - runs-on: ${{ matrix.runner || 'ubuntu-latest' }} + runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false', github.run_id) }} needs: [build, matrix] permissions: contents: read @@ -448,16 +448,7 @@ jobs: runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/family=c7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - - service: Sites - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - - service: Functions - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - - service: Avatars - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - - service: Realtime - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false - service: TablesDB - runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: Migrations From cf1bb1a1cc4e93fe869d54507466700574d73bb9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 11 May 2026 17:59:43 +0530 Subject: [PATCH 19/37] build From 5cd737b901714a95515258038b4630e1e619b5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 15:08:15 +0200 Subject: [PATCH 20/37] Backwards compatibility for GET /v1/project --- app/controllers/general.php | 3 +- src/Appwrite/Utopia/Response/Filters/V26.php | 141 ++++++++++++++++++ .../Projects/ProjectsConsoleClientTest.php | 24 +++ 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index f80bc3b631..f5490c3199 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -944,7 +944,8 @@ Http::init() $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { if (version_compare($responseFormat, '1.9.5', '<')) { - $response->addFilter(new ResponseV26()); + // TODO: Replace with proper dependency injection, likely should be passed from action itself + $response->addFilter(new ResponseV26($project)); } if (version_compare($responseFormat, '1.9.4', '<')) { $response->addFilter(new ResponseV25()); diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index c05f556cd0..43106ebb30 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -2,13 +2,154 @@ namespace Appwrite\Utopia\Response\Filters; +use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; +use Utopia\Config\Config; +use Utopia\Database\Document; // Convert 1.9.5 Data format to 1.9.4 format class V26 extends Filter { + public function __construct(protected Document $project) + { + } + public function parse(array $content, string $model): array { + return match ($model) { + Response::MODEL_PROJECT => $this->parseProject($content), + Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', fn ($item) => $this->parseProjectItem($item)), + default => $content, + }; + } + + private function parseProjectItem(array $content): array + { + $this->expandAuthMethods($content); + $this->expandServices($content); + $this->expandProtocols($content); + + unset($content['authMethods'], $content['services'], $content['protocols']); + return $content; } + + private function parseProject(array $content): array + { + $content = $this->parseProjectItem($content); + + $auths = $this->project->getAttribute('auths', []); + $content['authLimit'] = $auths['limit'] ?? 0; + $content['authDuration'] = $auths['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; + $content['authSessionsLimit'] = $auths['maxSessions'] ?? 0; + $content['authPasswordHistory'] = $auths['passwordHistory'] ?? 0; + $content['authPasswordDictionary'] = $auths['passwordDictionary'] ?? false; + $content['authPersonalDataCheck'] = $auths['personalDataCheck'] ?? false; + $content['authDisposableEmails'] = $auths['disposableEmails'] ?? false; + $content['authCanonicalEmails'] = $auths['canonicalEmails'] ?? false; + $content['authFreeEmails'] = $auths['freeEmails'] ?? false; + $content['authMockNumbers'] = $auths['mockNumbers'] ?? []; + $content['authSessionAlerts'] = $auths['sessionAlerts'] ?? false; + $content['authMembershipsUserName'] = $auths['membershipsUserName'] ?? false; + $content['authMembershipsUserEmail'] = $auths['membershipsUserEmail'] ?? false; + $content['authMembershipsMfa'] = $auths['membershipsMfa'] ?? false; + $content['authMembershipsUserId'] = $auths['membershipsUserId'] ?? false; + $content['authMembershipsUserPhone'] = $auths['membershipsUserPhone'] ?? false; + $content['authInvalidateSessions'] = $auths['invalidateSessions'] ?? false; + + $content['description'] = $this->project->getAttribute('description', ''); + $content['logo'] = $this->project->getAttribute('logo', ''); + $content['url'] = $this->project->getAttribute('url', ''); + $content['legalName'] = $this->project->getAttribute('legalName', ''); + $content['legalCountry'] = $this->project->getAttribute('legalCountry', ''); + $content['legalState'] = $this->project->getAttribute('legalState', ''); + $content['legalCity'] = $this->project->getAttribute('legalCity', ''); + $content['legalAddress'] = $this->project->getAttribute('legalAddress', ''); + $content['legalTaxId'] = $this->project->getAttribute('legalTaxId', ''); + + $content['oAuthProviders'] = $this->expandOAuthProviders(); + $content['platforms'] = $this->getDocumentArray($this->project->getAttribute('platforms', [])); + $content['webhooks'] = $this->getDocumentArray($this->project->getAttribute('webhooks', [])); + $content['keys'] = $this->getDocumentArray($this->project->getAttribute('keys', [])); + + return $content; + } + + private function expandAuthMethods(array &$content): void + { + $authMethods = []; + foreach ($content['authMethods'] ?? [] as $method) { + $authMethods[$method['$id'] ?? ''] = $method['enabled'] ?? true; + } + + foreach (Config::getParam('auth', []) as $id => $method) { + $key = $method['key'] ?? ''; + $content['auth' . ucfirst($key)] = $authMethods[$id] ?? true; + } + } + + private function expandServices(array &$content): void + { + $services = []; + foreach ($content['services'] ?? [] as $service) { + $services[$service['$id'] ?? ''] = $service['enabled'] ?? true; + } + + foreach (Config::getParam('services', []) as $id => $service) { + if (!($service['optional'] ?? false)) { + continue; + } + $key = $service['key'] ?? ''; + $content['serviceStatusFor' . ucfirst($key)] = $services[$id] ?? true; + } + } + + private function expandProtocols(array &$content): void + { + $protocols = []; + foreach ($content['protocols'] ?? [] as $protocol) { + $protocols[$protocol['$id'] ?? ''] = $protocol['enabled'] ?? true; + } + + foreach (Config::getParam('protocols', []) as $id => $api) { + $key = $api['key'] ?? ''; + $content['protocolStatusFor' . ucfirst($key)] = $protocols[$id] ?? true; + } + } + + private function expandOAuthProviders(): array + { + $providers = Config::getParam('oAuthProviders', []); + $providerValues = $this->project->getAttribute('oAuthProviders', []); + $projectProviders = []; + + foreach ($providers as $key => $provider) { + if (!($provider['enabled'] ?? false)) { + continue; + } + + $projectProviders[] = [ + 'key' => $key, + 'name' => $provider['name'] ?? '', + 'appId' => $providerValues[$key . 'Appid'] ?? '', + 'secret' => '', + 'enabled' => $providerValues[$key . 'Enabled'] ?? false, + ]; + } + + return $projectProviders; + } + + private function getDocumentArray(array $documents): array + { + $result = []; + foreach ($documents as $document) { + if ($document instanceof Document) { + $result[] = $document->getArrayCopy(); + } elseif (\is_array($document)) { + $result[] = $document; + } + } + return $result; + } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 8b0c1af57f..01bd101b1e 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -210,6 +210,7 @@ class ProjectsConsoleClientTest extends Scope $getProject = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(404, $getProject['headers']['status-code']); @@ -1027,6 +1028,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -1411,6 +1413,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -1482,6 +1485,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/empty', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); @@ -1490,6 +1494,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/'.$projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(400, $response['headers']['status-code']); @@ -1605,6 +1610,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -1932,6 +1938,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertTrue($response['body']['authSessionAlerts']); @@ -2054,6 +2061,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2166,6 +2174,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2203,6 +2212,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2220,6 +2230,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2237,6 +2248,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2293,6 +2305,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2334,6 +2347,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2445,6 +2459,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -3358,6 +3373,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3387,6 +3403,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3442,6 +3459,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3464,6 +3482,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3527,6 +3546,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3603,6 +3623,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -3679,6 +3700,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ])); @@ -5510,6 +5532,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $project['headers']['status-code']); @@ -5533,6 +5556,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(404, $project['headers']['status-code']); From 870fe72bf6e16e804e20357ddd146e747fa4f575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 15:40:45 +0200 Subject: [PATCH 21/37] Fix response filter --- src/Appwrite/Utopia/Response.php | 5 +- src/Appwrite/Utopia/Response/Filter.php | 9 +++ src/Appwrite/Utopia/Response/Filters/V26.php | 72 ++++++++------------ 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 01f86cb5e8..c9e8800569 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -438,9 +438,10 @@ class Response extends SwooleResponse return isset(self::$models[$key]); } - public function applyFilters(array $data, string $model): array + public function applyFilters(array $data, string $model, array $raw): array { foreach ($this->filters as $filter) { + $filter->setRawContent($raw); $data = $filter->parse($data, $model); } @@ -460,7 +461,7 @@ class Response extends SwooleResponse public function dynamic(Document $document, string $model): void { $output = $this->output(clone $document, $model); - $output = $this->applyFilters($output, $model); + $output = $this->applyFilters($output, $model, raw: clone $document); switch ($this->getContentType()) { case self::CONTENT_TYPE_JSON: diff --git a/src/Appwrite/Utopia/Response/Filter.php b/src/Appwrite/Utopia/Response/Filter.php index bd82467f81..b170ccd6ef 100644 --- a/src/Appwrite/Utopia/Response/Filter.php +++ b/src/Appwrite/Utopia/Response/Filter.php @@ -4,6 +4,11 @@ namespace Appwrite\Utopia\Response; abstract class Filter { + /** + * @var ?array $rawContent + */ + protected ?array $rawContent = null; + /** * Parse the content to another format. * @@ -14,6 +19,10 @@ abstract class Filter */ abstract public function parse(array $content, string $model): array; + public function setRawContent(array $rawContent): void + { + $this->rawContent = $rawContent; + } /** * Handle list diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index 43106ebb30..1aa6c0c821 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -5,25 +5,33 @@ namespace Appwrite\Utopia\Response\Filters; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; use Utopia\Config\Config; -use Utopia\Database\Document; // Convert 1.9.5 Data format to 1.9.4 format class V26 extends Filter { - public function __construct(protected Document $project) - { - } - public function parse(array $content, string $model): array { return match ($model) { - Response::MODEL_PROJECT => $this->parseProject($content), - Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', fn ($item) => $this->parseProjectItem($item)), + Response::MODEL_PROJECT => $this->parseProject($content, $this->rawContent), + Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', function ($item) { + $projectId = $item['$id'] ?? ''; + + $rawProjects = $this->rawContent['projects'] ?? []; + $rawProject = null; + foreach ($rawProjects as $rawItem) { + if ($rawItem['$id'] === $projectId) { + $rawProject = $rawItem; + break; + } + } + + return $this->parseProject($item, $rawProject); + }), default => $content, }; } - private function parseProjectItem(array $content): array + private function parseProject(array $content, array $raw): array { $this->expandAuthMethods($content); $this->expandServices($content); @@ -31,14 +39,7 @@ class V26 extends Filter unset($content['authMethods'], $content['services'], $content['protocols']); - return $content; - } - - private function parseProject(array $content): array - { - $content = $this->parseProjectItem($content); - - $auths = $this->project->getAttribute('auths', []); + $auths = $raw['auths'] ?? []; $content['authLimit'] = $auths['limit'] ?? 0; $content['authDuration'] = $auths['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $content['authSessionsLimit'] = $auths['maxSessions'] ?? 0; @@ -57,20 +58,20 @@ class V26 extends Filter $content['authMembershipsUserPhone'] = $auths['membershipsUserPhone'] ?? false; $content['authInvalidateSessions'] = $auths['invalidateSessions'] ?? false; - $content['description'] = $this->project->getAttribute('description', ''); - $content['logo'] = $this->project->getAttribute('logo', ''); - $content['url'] = $this->project->getAttribute('url', ''); - $content['legalName'] = $this->project->getAttribute('legalName', ''); - $content['legalCountry'] = $this->project->getAttribute('legalCountry', ''); - $content['legalState'] = $this->project->getAttribute('legalState', ''); - $content['legalCity'] = $this->project->getAttribute('legalCity', ''); - $content['legalAddress'] = $this->project->getAttribute('legalAddress', ''); - $content['legalTaxId'] = $this->project->getAttribute('legalTaxId', ''); + $content['description'] = $raw['description'] ?? ''; + $content['logo'] = $raw['logo'] ?? ''; + $content['url'] = $raw['url'] ?? ''; + $content['legalName'] = $raw['legalName'] ?? ''; + $content['legalCountry'] = $raw['legalCountry'] ?? ''; + $content['legalState'] = $raw['legalState'] ?? ''; + $content['legalCity'] = $raw['legalCity'] ?? ''; + $content['legalAddress'] = $raw['legalAddress'] ?? ''; + $content['legalTaxId'] = $raw['legalTaxId'] ?? ''; $content['oAuthProviders'] = $this->expandOAuthProviders(); - $content['platforms'] = $this->getDocumentArray($this->project->getAttribute('platforms', [])); - $content['webhooks'] = $this->getDocumentArray($this->project->getAttribute('webhooks', [])); - $content['keys'] = $this->getDocumentArray($this->project->getAttribute('keys', [])); + $content['platforms'] = $raw['platforms'] ?? []; + $content['webhooks'] = $raw['webhooks'] ?? []; + $content['keys'] = $raw['keys'] ?? []; return $content; } @@ -120,7 +121,7 @@ class V26 extends Filter private function expandOAuthProviders(): array { $providers = Config::getParam('oAuthProviders', []); - $providerValues = $this->project->getAttribute('oAuthProviders', []); + $providerValues = $this->rawContent['oAuthProviders'] ?? []; $projectProviders = []; foreach ($providers as $key => $provider) { @@ -139,17 +140,4 @@ class V26 extends Filter return $projectProviders; } - - private function getDocumentArray(array $documents): array - { - $result = []; - foreach ($documents as $document) { - if ($document instanceof Document) { - $result[] = $document->getArrayCopy(); - } elseif (\is_array($document)) { - $result[] = $document; - } - } - return $result; - } } From 89ec639f87483e54a6b9c9d7c160e588eb3ddfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 15:55:53 +0200 Subject: [PATCH 22/37] Fix startup issues --- src/Appwrite/Utopia/Response.php | 2 +- src/Appwrite/Utopia/Response/Filter.php | 8 ++- src/Appwrite/Utopia/Response/Filters/V26.php | 67 ++++++++++---------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index c9e8800569..a0c2a12234 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -438,7 +438,7 @@ class Response extends SwooleResponse return isset(self::$models[$key]); } - public function applyFilters(array $data, string $model, array $raw): array + public function applyFilters(array $data, string $model, Document $raw): array { foreach ($this->filters as $filter) { $filter->setRawContent($raw); diff --git a/src/Appwrite/Utopia/Response/Filter.php b/src/Appwrite/Utopia/Response/Filter.php index b170ccd6ef..13833be328 100644 --- a/src/Appwrite/Utopia/Response/Filter.php +++ b/src/Appwrite/Utopia/Response/Filter.php @@ -2,12 +2,14 @@ namespace Appwrite\Utopia\Response; +use Utopia\Database\Document; + abstract class Filter { /** - * @var ?array $rawContent + * @var ?Document $rawContent */ - protected ?array $rawContent = null; + protected ?Document $rawContent = null; /** * Parse the content to another format. @@ -19,7 +21,7 @@ abstract class Filter */ abstract public function parse(array $content, string $model): array; - public function setRawContent(array $rawContent): void + public function setRawContent(Document $rawContent): void { $this->rawContent = $rawContent; } diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index 1aa6c0c821..e892f5a40e 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -5,6 +5,7 @@ namespace Appwrite\Utopia\Response\Filters; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; use Utopia\Config\Config; +use Utopia\Database\Document; // Convert 1.9.5 Data format to 1.9.4 format class V26 extends Filter @@ -16,10 +17,10 @@ class V26 extends Filter Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', function ($item) { $projectId = $item['$id'] ?? ''; - $rawProjects = $this->rawContent['projects'] ?? []; + $rawProjects = $this->rawContent->getAttribute('projects', []); $rawProject = null; foreach ($rawProjects as $rawItem) { - if ($rawItem['$id'] === $projectId) { + if ($rawItem->getId() === $projectId) { $rawProject = $rawItem; break; } @@ -31,7 +32,7 @@ class V26 extends Filter }; } - private function parseProject(array $content, array $raw): array + private function parseProject(array $content, Document $raw): array { $this->expandAuthMethods($content); $this->expandServices($content); @@ -39,39 +40,39 @@ class V26 extends Filter unset($content['authMethods'], $content['services'], $content['protocols']); - $auths = $raw['auths'] ?? []; - $content['authLimit'] = $auths['limit'] ?? 0; - $content['authDuration'] = $auths['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; - $content['authSessionsLimit'] = $auths['maxSessions'] ?? 0; - $content['authPasswordHistory'] = $auths['passwordHistory'] ?? 0; - $content['authPasswordDictionary'] = $auths['passwordDictionary'] ?? false; - $content['authPersonalDataCheck'] = $auths['personalDataCheck'] ?? false; - $content['authDisposableEmails'] = $auths['disposableEmails'] ?? false; - $content['authCanonicalEmails'] = $auths['canonicalEmails'] ?? false; - $content['authFreeEmails'] = $auths['freeEmails'] ?? false; - $content['authMockNumbers'] = $auths['mockNumbers'] ?? []; - $content['authSessionAlerts'] = $auths['sessionAlerts'] ?? false; - $content['authMembershipsUserName'] = $auths['membershipsUserName'] ?? false; - $content['authMembershipsUserEmail'] = $auths['membershipsUserEmail'] ?? false; - $content['authMembershipsMfa'] = $auths['membershipsMfa'] ?? false; - $content['authMembershipsUserId'] = $auths['membershipsUserId'] ?? false; - $content['authMembershipsUserPhone'] = $auths['membershipsUserPhone'] ?? false; - $content['authInvalidateSessions'] = $auths['invalidateSessions'] ?? false; + $auths = $raw->getAttribute('auths', []); + $content['authLimit'] = $auths->getAttribute('limit', 0); + $content['authDuration'] = $auths->getAttribute('duration', TOKEN_EXPIRATION_LOGIN_LONG); + $content['authSessionsLimit'] = $auths->getAttribute('maxSessions', 0); + $content['authPasswordHistory'] = $auths->getAttribute('passwordHistory', 0); + $content['authPasswordDictionary'] = $auths->getAttribute('passwordDictionary', false); + $content['authPersonalDataCheck'] = $auths->getAttribute('personalDataCheck', false); + $content['authDisposableEmails'] = $auths->getAttribute('disposableEmails', false); + $content['authCanonicalEmails'] = $auths->getAttribute('canonicalEmails', false); + $content['authFreeEmails'] = $auths->getAttribute('freeEmails', false); + $content['authMockNumbers'] = $auths->getAttribute('mockNumbers', []); + $content['authSessionAlerts'] = $auths->getAttribute('sessionAlerts', false); + $content['authMembershipsUserName'] = $auths->getAttribute('membershipsUserName', false); + $content['authMembershipsUserEmail'] = $auths->getAttribute('membershipsUserEmail', false); + $content['authMembershipsMfa'] = $auths->getAttribute('membershipsMfa', false); + $content['authMembershipsUserId'] = $auths->getAttribute('membershipsUserId', false); + $content['authMembershipsUserPhone'] = $auths->getAttribute('membershipsUserPhone', false); + $content['authInvalidateSessions'] = $auths->getAttribute('invalidateSessions', false); - $content['description'] = $raw['description'] ?? ''; - $content['logo'] = $raw['logo'] ?? ''; - $content['url'] = $raw['url'] ?? ''; - $content['legalName'] = $raw['legalName'] ?? ''; - $content['legalCountry'] = $raw['legalCountry'] ?? ''; - $content['legalState'] = $raw['legalState'] ?? ''; - $content['legalCity'] = $raw['legalCity'] ?? ''; - $content['legalAddress'] = $raw['legalAddress'] ?? ''; - $content['legalTaxId'] = $raw['legalTaxId'] ?? ''; + $content['description'] = $raw->getAttribute('description', ''); + $content['logo'] = $raw->getAttribute('logo', ''); + $content['url'] = $raw->getAttribute('url', ''); + $content['legalName'] = $raw->getAttribute('legalName', ''); + $content['legalCountry'] = $raw->getAttribute('legalCountry', ''); + $content['legalState'] = $raw->getAttribute('legalState', ''); + $content['legalCity'] = $raw->getAttribute('legalCity', ''); + $content['legalAddress'] = $raw->getAttribute('legalAddress', ''); + $content['legalTaxId'] = $raw->getAttribute('legalTaxId', ''); $content['oAuthProviders'] = $this->expandOAuthProviders(); - $content['platforms'] = $raw['platforms'] ?? []; - $content['webhooks'] = $raw['webhooks'] ?? []; - $content['keys'] = $raw['keys'] ?? []; + $content['platforms'] = $raw->getAttribute('platforms', []); + $content['webhooks'] = $raw->getAttribute('webhooks', []); + $content['keys'] = $raw->getAttribute('keys', []); return $content; } From 6cbf080156ef58cfed912f394640368b6fdd28fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 16:04:39 +0200 Subject: [PATCH 23/37] Fix code quality --- src/Appwrite/Utopia/Response/Filters/V26.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index e892f5a40e..d1d1a5b908 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -40,7 +40,7 @@ class V26 extends Filter unset($content['authMethods'], $content['services'], $content['protocols']); - $auths = $raw->getAttribute('auths', []); + $auths = new Document($raw->getAttribute('auths', [])); $content['authLimit'] = $auths->getAttribute('limit', 0); $content['authDuration'] = $auths->getAttribute('duration', TOKEN_EXPIRATION_LOGIN_LONG); $content['authSessionsLimit'] = $auths->getAttribute('maxSessions', 0); @@ -69,7 +69,7 @@ class V26 extends Filter $content['legalAddress'] = $raw->getAttribute('legalAddress', ''); $content['legalTaxId'] = $raw->getAttribute('legalTaxId', ''); - $content['oAuthProviders'] = $this->expandOAuthProviders(); + $content['oAuthProviders'] = $this->expandOAuthProviders($raw); $content['platforms'] = $raw->getAttribute('platforms', []); $content['webhooks'] = $raw->getAttribute('webhooks', []); $content['keys'] = $raw->getAttribute('keys', []); @@ -119,10 +119,10 @@ class V26 extends Filter } } - private function expandOAuthProviders(): array + private function expandOAuthProviders(Document $raw): array { $providers = Config::getParam('oAuthProviders', []); - $providerValues = $this->rawContent['oAuthProviders'] ?? []; + $providerValues = $raw->getAttribute('oAuthProviders', []); $projectProviders = []; foreach ($providers as $key => $provider) { From 9056de0103b1a3af9cb02c2c45be7a302a176531 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Mon, 11 May 2026 19:38:06 +0530 Subject: [PATCH 24/37] fix codex plugin --- app/config/sdks.php | 2 +- composer.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/config/sdks.php b/app/config/sdks.php index cbfe76dce4..e29b28690f 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -323,7 +323,7 @@ return [ [ 'key' => 'codex-plugin', 'name' => 'CodexPlugin', - 'version' => '0.1.0', + 'version' => '0.1.1', 'url' => 'https://github.com/appwrite/codex-plugin.git', 'enabled' => true, 'beta' => false, diff --git a/composer.lock b/composer.lock index 0aa58ca03e..c217939534 100644 --- a/composer.lock +++ b/composer.lock @@ -3614,16 +3614,16 @@ }, { "name": "utopia-php/cache", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "d36f9050c39c02e09a7763389c9e71258e74af1f" + "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/d36f9050c39c02e09a7763389c9e71258e74af1f", - "reference": "d36f9050c39c02e09a7763389c9e71258e74af1f", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa", + "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa", "shasum": "" }, "require": { @@ -3660,9 +3660,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/1.0.2" + "source": "https://github.com/utopia-php/cache/tree/1.0.3" }, - "time": "2026-05-08T11:40:20+00:00" + "time": "2026-05-11T11:02:13+00:00" }, { "name": "utopia-php/cli", @@ -5476,16 +5476,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.28.1", + "version": "1.28.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "009118ccda8ccece2b9fc043c158cb1dd3efaa88" + "reference": "38de925e8c9e7f0f720d45187be54a291aaf696b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/009118ccda8ccece2b9fc043c158cb1dd3efaa88", - "reference": "009118ccda8ccece2b9fc043c158cb1dd3efaa88", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/38de925e8c9e7f0f720d45187be54a291aaf696b", + "reference": "38de925e8c9e7f0f720d45187be54a291aaf696b", "shasum": "" }, "require": { @@ -5521,9 +5521,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.28.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.28.4" }, - "time": "2026-05-08T13:24:33+00:00" + "time": "2026-05-11T13:55:49+00:00" }, { "name": "brianium/paratest", From 5fba2889cb3429bd598c2e4a02cc994a0a18c85c Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 16:58:28 +0200 Subject: [PATCH 25/37] chore: update e2e_service runner configuration to use m7 family --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de36bc42f..6f81987c47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -406,7 +406,7 @@ jobs: e2e_service: name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }} - runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/family=c7/volume=120g/spot=false', github.run_id) }} + runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/family=m7/volume=120g/spot=false', github.run_id) }} needs: [build, matrix] permissions: contents: read @@ -445,7 +445,7 @@ jobs: ] include: - service: Databases - runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/family=c7/volume=120g/spot=false + runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/family=m7/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: TablesDB From 152b45087ee7ecb18c1410d2d827a581ae3b8010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 17:01:00 +0200 Subject: [PATCH 26/37] Fix more tests --- app/controllers/general.php | 3 +- phpunit.xml | 2 +- src/Appwrite/Utopia/Response/Filters/V26.php | 23 ++++- .../Utopia/Response/Model/Project.php | 2 +- .../Projects/ProjectsConsoleClientTest.php | 96 +++++++++++++++++-- tests/unit/Utopia/ResponseTest.php | 5 +- 6 files changed, 114 insertions(+), 17 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index f5490c3199..f80bc3b631 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -944,8 +944,7 @@ Http::init() $responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); if ($responseFormat) { if (version_compare($responseFormat, '1.9.5', '<')) { - // TODO: Replace with proper dependency injection, likely should be passed from action itself - $response->addFilter(new ResponseV26($project)); + $response->addFilter(new ResponseV26()); } if (version_compare($responseFormat, '1.9.4', '<')) { $response->addFilter(new ResponseV25()); diff --git a/phpunit.xml b/phpunit.xml index 9748c5a5c8..b566e232cd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,7 +5,7 @@ bootstrap="app/init.php" colors="true" processIsolation="false" - stopOnFailure="false" + stopOnFailure="true" stopOnError="false" cacheDirectory=".phpunit.cache" > diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index d1d1a5b908..2d88c95be5 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -71,7 +71,28 @@ class V26 extends Filter $content['oAuthProviders'] = $this->expandOAuthProviders($raw); $content['platforms'] = $raw->getAttribute('platforms', []); - $content['webhooks'] = $raw->getAttribute('webhooks', []); + + $content['webhooks'] = []; + foreach ($raw->getAttribute('webhooks', []) as $webhook) { + + $webhook->setAttribute('tls', $webhook->getAttribute('security', true)); + $webhook->removeAttribute('security'); + + + $webhook->setAttribute('authUsername', $webhook->getAttribute('httpUser', '')); + $webhook->removeAttribute('httpUser'); + + + $webhook->setAttribute('authPassword', $webhook->getAttribute('httpPass', '')); + $webhook->removeAttribute('httpPass'); + + + $webhook->setAttribute('secret', $webhook->getAttribute('signatureKey', '')); + $webhook->removeAttribute('signatureKey'); + + $content['webhooks'][] = $webhook; + } + $content['keys'] = $raw->getAttribute('keys', []); return $content; diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 9998c35f2e..af2a21d551 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -47,7 +47,7 @@ class Project extends Model // Resource: Dev Keys ->addRule('devKeys', [ 'type' => Response::MODEL_DEV_KEY, - 'description' => 'List of dev keys.', + 'description' => 'Deprecated since 1.9.5: List of dev keys.', 'default' => [], 'example' => new \stdClass(), 'array' => true, diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 01bd101b1e..aa5e6911f1 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -46,6 +46,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4' ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -68,6 +69,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -89,6 +91,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => '', @@ -101,6 +104,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -127,6 +131,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'MultiDB Project', @@ -235,6 +240,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => $projectId, 'name' => 'Original Project', @@ -250,6 +256,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => $projectId, 'name' => 'Project Duplicate', @@ -299,6 +306,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Team 1 Project', @@ -319,6 +327,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/team', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'teamId' => $team2, ]); @@ -342,6 +351,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -354,6 +364,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders(), [ 'search' => $id ])); @@ -365,6 +376,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders(), [ 'search' => 'Project Test' ])); @@ -391,6 +403,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test 2', @@ -409,6 +422,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::equal('teamId', [$team['body']['$id']])->toString(), @@ -423,6 +437,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::limit(1)->toString(), @@ -436,6 +451,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::offset(1)->toString(), @@ -448,6 +464,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::equal('name', ['Project Test 2'])->toString(), @@ -462,6 +479,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::orderDesc()->toString(), @@ -475,6 +493,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -484,6 +503,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(), @@ -499,6 +519,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), @@ -525,6 +546,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Query Select Test Project', @@ -541,6 +563,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -570,6 +593,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4' ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(), @@ -601,6 +625,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'teamId'])->toString(), @@ -632,6 +657,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name'])->toString(), @@ -662,6 +688,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'platforms'])->toString(), @@ -691,6 +718,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(), @@ -720,6 +748,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['*'])->toString(), @@ -750,6 +779,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::select(['$id', 'invalidAttribute'])->toString(), @@ -784,6 +814,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -1043,17 +1074,17 @@ class ProjectsConsoleClientTest extends Scope $this->assertNotEmpty($response['body']['$updatedAt']); $this->assertNotFalse(\strtotime($response['body']['$updatedAt'])); - $this->assertEquals('My description', $response['body']['description']); + // $this->assertEquals('My description', $response['body']['description']); // No longer supported $this->assertEquals($team['body']['$id'], $response['body']['teamId']); $this->assertEquals('active', $response['body']['status']); - $this->assertEquals('https://google.com/logo.png', $response['body']['logo']); - $this->assertEquals('https://myapp.com/', $response['body']['url']); - $this->assertEquals('Legal company', $response['body']['legalName']); - $this->assertEquals('Slovakia', $response['body']['legalCountry']); - $this->assertEquals('Custom state', $response['body']['legalState']); - $this->assertEquals('Košice', $response['body']['legalCity']); - $this->assertEquals('Main street 32', $response['body']['legalAddress']); - $this->assertEquals('TAXID_123456', $response['body']['legalTaxId']); + // $this->assertEquals('https://google.com/logo.png', $response['body']['logo']); // No longer supported + // $this->assertEquals('https://myapp.com/', $response['body']['url']); // No longer supported + // $this->assertEquals('Legal company', $response['body']['legalName']); // No longer supported + // $this->assertEquals('Slovakia', $response['body']['legalCountry']); // No longer supported + // $this->assertEquals('Custom state', $response['body']['legalState']); // No longer supported + // $this->assertEquals('Košice', $response['body']['legalCity']); // No longer supported + // $this->assertEquals('Main street 32', $response['body']['legalAddress']); // No longer supported + // $this->assertEquals('TAXID_123456', $response['body']['legalTaxId']); // No longer supported $this->assertEquals(135, $response['body']['authDuration']); $this->assertEquals(54, $response['body']['authLimit']); $this->assertEquals(7, $response['body']['authSessionsLimit']); @@ -1497,7 +1528,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders())); - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(404, $response['headers']['status-code']); } public function testGetProjectUsage(): void @@ -1525,6 +1556,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -1541,6 +1573,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test 2', @@ -1561,6 +1594,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => '', @@ -1585,6 +1619,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'enabled' => true, 'senderEmail' => 'mailer@appwrite.io', @@ -1630,6 +1665,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'enabled' => true, 'senderEmail' => 'fail@appwrite.io', @@ -1669,6 +1705,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -1857,6 +1894,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Session Alert Locale Fallback Test', @@ -1870,6 +1908,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/smtp', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'enabled' => true, 'senderEmail' => 'mailer@appwrite.io', @@ -2198,6 +2237,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Session Invalidation Test Project', @@ -2273,6 +2313,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -2292,6 +2333,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'provider' => $key, 'appId' => 'AppId-' . ucfirst($key), @@ -2333,6 +2375,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'provider' => $key, 'enabled' => $i === 0 ? false : true // On first provider, test enabled=false @@ -2377,6 +2420,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'provider' => 'unknown', 'appId' => 'AppId', @@ -2404,6 +2448,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test', @@ -2870,6 +2915,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), []); $this->assertEquals(400, $response['headers']['status-code']); @@ -2879,6 +2925,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ 'phone' => '+1655513432', @@ -2892,6 +2939,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2907,6 +2955,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2922,6 +2971,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2937,6 +2987,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2952,6 +3003,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2967,6 +3019,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -2994,6 +3047,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => $numbers ]); @@ -3007,6 +3061,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [] ]); @@ -3015,6 +3070,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'numbers' => [ [ @@ -3338,6 +3394,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ]), [ 'projectId' => ID::unique(), @@ -3429,6 +3486,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ]), [ 'projectId' => ID::unique(), @@ -3506,6 +3564,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ]), [ 'projectId' => ID::unique(), @@ -4459,6 +4518,7 @@ class ProjectsConsoleClientTest extends Scope $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Project Test 2', @@ -5507,6 +5567,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Amazing Project', @@ -5580,6 +5641,7 @@ class ProjectsConsoleClientTest extends Scope $project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Amazing Project 1', @@ -5590,6 +5652,7 @@ class ProjectsConsoleClientTest extends Scope $project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Amazing Project 2', @@ -6932,6 +6995,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Test project - Labels 1', @@ -6976,6 +7040,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip'])->toString(), @@ -6988,6 +7053,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -6999,6 +7065,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7011,6 +7078,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['nonvip', 'imagine'])->toString(), @@ -7024,6 +7092,7 @@ class ProjectsConsoleClientTest extends Scope $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'projectId' => ID::unique(), 'name' => 'Test project - Labels 2', @@ -7052,6 +7121,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['imagine'])->toString(), @@ -7066,6 +7136,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7079,6 +7150,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip'])->toString(), @@ -7093,6 +7165,7 @@ class ProjectsConsoleClientTest extends Scope $projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()), [ 'queries' => [ Query::contains('labels', ['vip', 'imagine'])->toString(), @@ -7198,6 +7271,7 @@ class ProjectsConsoleClientTest extends Scope 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ]); @@ -7294,6 +7368,7 @@ class ProjectsConsoleClientTest extends Scope 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ 'name' => $newProjectName, @@ -7310,6 +7385,7 @@ class ProjectsConsoleClientTest extends Scope 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token, ], [ 'name' => $newProjectName, diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php index f5a30a5500..74c68303f4 100644 --- a/tests/unit/Utopia/ResponseTest.php +++ b/tests/unit/Utopia/ResponseTest.php @@ -34,10 +34,11 @@ class ResponseTest extends TestCase $this->assertTrue($this->response->hasFilters()); $this->assertCount(2, $this->response->getFilters()); - $output = $this->response->applyFilters([ + $content = [ 'initial' => true, 'first' => false - ], 'test'); + ]; + $output = $this->response->applyFilters($content, 'test', raw: new Document($content)); $this->assertArrayHasKey('initial', $output); $this->assertTrue($output['initial']); From 5eb4c6b3102362cdc913f58253c6e47514cfb8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 18:38:30 +0200 Subject: [PATCH 27/37] Fix tests --- tests/e2e/Services/Project/AuthMethodsBase.php | 5 ++++- tests/e2e/Services/Project/PoliciesBase.php | 5 ++++- .../PoliciesMembershipPrivacyIntegrationTest.php | 1 + .../PoliciesPasswordDictionaryIntegrationTest.php | 1 + .../Project/PoliciesPasswordHistoryIntegrationTest.php | 1 + .../PoliciesPasswordPersonalDataIntegrationTest.php | 1 + .../Project/PoliciesSessionAlertIntegrationTest.php | 1 + .../Project/PoliciesSessionDurationIntegrationTest.php | 1 + .../PoliciesSessionInvalidationIntegrationTest.php | 1 + .../Project/PoliciesUserLimitIntegrationTest.php | 1 + tests/e2e/Services/Project/ProtocolsBase.php | 5 ++++- tests/e2e/Services/Project/ServicesBase.php | 5 ++++- .../Services/Projects/ProjectsConsoleClientTest.php | 10 ++++++++++ 13 files changed, 34 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Project/AuthMethodsBase.php b/tests/e2e/Services/Project/AuthMethodsBase.php index afa58a3640..cccdca9ea1 100644 --- a/tests/e2e/Services/Project/AuthMethodsBase.php +++ b/tests/e2e/Services/Project/AuthMethodsBase.php @@ -212,6 +212,7 @@ trait AuthMethodsBase $headers = \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()); $projectId = $this->getProject()['$id']; @@ -322,7 +323,9 @@ trait AuthMethodsBase ]; if ($authenticated) { - $headers = \array_merge($headers, $this->getHeaders()); + $headers = \array_merge($headers, $this->getHeaders(), [ + 'x-appwrite-response-format' => '1.9.4', + ]); } return $this->client->call( diff --git a/tests/e2e/Services/Project/PoliciesBase.php b/tests/e2e/Services/Project/PoliciesBase.php index 04906c6c2b..43c09b55c3 100644 --- a/tests/e2e/Services/Project/PoliciesBase.php +++ b/tests/e2e/Services/Project/PoliciesBase.php @@ -1082,7 +1082,9 @@ trait PoliciesBase ]; if ($authenticated) { - $headers = array_merge($headers, $this->getHeaders()); + $headers = array_merge($headers, $this->getHeaders(), [ + 'x-appwrite-response-format' => '1.9.4', + ]); } return $headers; @@ -1094,6 +1096,7 @@ trait PoliciesBase 'content-type' => 'application/json', 'x-appwrite-project' => 'console', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-response-format' => '1.9.4', ]); } diff --git a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php index 378cd1800d..95d96c49e2 100644 --- a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesMembershipPrivacyIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; // Step 1: Configure privacy to false diff --git a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php index 2d0e15a70f..0da64eb50b 100644 --- a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesPasswordDictionaryIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; // "password" is the top entry in the common-passwords dictionary and is 8 chars (min length). diff --git a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php index c2dfd7be5e..1027d88222 100644 --- a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesPasswordHistoryIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; // Step 1: Enable password history policy with limit 3 diff --git a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php index 3284fed16f..ebb94a2631 100644 --- a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php @@ -21,6 +21,7 @@ class PoliciesPasswordPersonalDataIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; $setPersonalData = function (bool $enabled) use ($serverHeaders): void { diff --git a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php index 1500a1dcfa..3e847b9ddb 100644 --- a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php @@ -23,6 +23,7 @@ class PoliciesSessionAlertIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; $publicHeaders = [ diff --git a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php index 71562f52a5..582da65373 100644 --- a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesSessionDurationIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; $publicHeaders = [ diff --git a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php index c9de2be9a5..b6f972ca3e 100644 --- a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesSessionInvalidationIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; $publicHeaders = [ diff --git a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php index 5ddcd8aaa1..6025d80536 100644 --- a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php +++ b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php @@ -22,6 +22,7 @@ class PoliciesUserLimitIntegrationTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $apiKey, + 'x-appwrite-response-format' => '1.9.4', ]; $signupHeaders = [ diff --git a/tests/e2e/Services/Project/ProtocolsBase.php b/tests/e2e/Services/Project/ProtocolsBase.php index f828994ea3..092ed97b6a 100644 --- a/tests/e2e/Services/Project/ProtocolsBase.php +++ b/tests/e2e/Services/Project/ProtocolsBase.php @@ -248,6 +248,7 @@ trait ProtocolsBase $headers = array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()); // Disable via the legacy `/status` alias @@ -278,7 +279,9 @@ trait ProtocolsBase ]; if ($authenticated) { - $headers = array_merge($headers, $this->getHeaders()); + $headers = array_merge($headers, $this->getHeaders(), [ + 'x-appwrite-response-format' => '1.9.4', + ]); } return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [ diff --git a/tests/e2e/Services/Project/ServicesBase.php b/tests/e2e/Services/Project/ServicesBase.php index b5f94f8181..0c8b337315 100644 --- a/tests/e2e/Services/Project/ServicesBase.php +++ b/tests/e2e/Services/Project/ServicesBase.php @@ -246,6 +246,7 @@ trait ServicesBase $headers = array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()); // Disable via the legacy `/status` alias @@ -276,7 +277,9 @@ trait ServicesBase ]; if ($authenticated) { - $headers = array_merge($headers, $this->getHeaders()); + $headers = array_merge($headers, $this->getHeaders(), [ + 'x-appwrite-response-format' => '1.9.4', + ]); } return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId, $headers, [ diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index aa5e6911f1..3ba143bef4 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1788,6 +1788,16 @@ class ProjectsConsoleClientTest extends Scope $data = $this->setupProjectData(); $id = $data['projectId']; + /** Ensure SMTP is disabled */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-response-format' => '1.9.4', + ], $this->getHeaders()), [ + 'enabled' => false, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + /** Get Default Email Template */ $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([ 'content-type' => 'application/json', From 36dec7c88f93f7320890a7723fff782f4c1b2ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 19:47:57 +0200 Subject: [PATCH 28/37] Fix more tests --- src/Appwrite/Utopia/Response/Filters/V26.php | 2 +- tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index 2d88c95be5..58ec380db8 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -18,7 +18,7 @@ class V26 extends Filter $projectId = $item['$id'] ?? ''; $rawProjects = $this->rawContent->getAttribute('projects', []); - $rawProject = null; + $rawProject = new Document(); foreach ($rawProjects as $rawItem) { if ($rawItem->getId() === $projectId) { $rawProject = $rawItem; diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php index f86557a432..7b0479ea09 100644 --- a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php +++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php @@ -28,6 +28,7 @@ class OAuthGitHubIntegrationTest extends Scope 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', + 'x-appwrite-response-format' => '1.9.4', ]; // Step 1: Create new organization (team) From 994a4e0fc7a0010d22dbeab81ace6c8c2e990ee6 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Mon, 11 May 2026 20:11:08 +0200 Subject: [PATCH 29/37] chore: remove 'family=m7' from e2e_service runner configuration --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f81987c47..ba7c4cc1be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -406,7 +406,7 @@ jobs: e2e_service: name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }} - runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/family=m7/volume=120g/spot=false', github.run_id) }} + runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/volume=120g/spot=false', github.run_id) }} needs: [build, matrix] permissions: contents: read @@ -445,7 +445,7 @@ jobs: ] include: - service: Databases - runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/family=m7/volume=120g/spot=false + runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false paratest_processes: 3 timeout_minutes: 30 - service: TablesDB From da3a3b939a0d767a1fae4db73e5fb5f869a165d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 11 May 2026 20:22:50 +0200 Subject: [PATCH 30/37] Finalize PR --- phpunit.xml | 2 +- src/Appwrite/Utopia/Request/Filters/V26.php | 23 +++++++++++++++++++ .../Projects/ProjectsConsoleClientTest.php | 10 -------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index b566e232cd..9748c5a5c8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,7 +5,7 @@ bootstrap="app/init.php" colors="true" processIsolation="false" - stopOnFailure="true" + stopOnFailure="false" stopOnError="false" cacheDirectory=".phpunit.cache" > diff --git a/src/Appwrite/Utopia/Request/Filters/V26.php b/src/Appwrite/Utopia/Request/Filters/V26.php index 62406ca519..00310ddff9 100644 --- a/src/Appwrite/Utopia/Request/Filters/V26.php +++ b/src/Appwrite/Utopia/Request/Filters/V26.php @@ -9,6 +9,29 @@ class V26 extends Filter // Convert 1.9.4 params to 1.9.5 public function parse(array $content, string $model): array { + switch ($model) { + case 'projects.create': + $content = $this->stripProjectMetadata($content); + break; + } + + return $content; + } + + protected function stripProjectMetadata(array $content): array + { + unset( + $content['description'], + $content['logo'], + $content['url'], + $content['legalName'], + $content['legalCountry'], + $content['legalState'], + $content['legalCity'], + $content['legalAddress'], + $content['legalTaxId'], + ); + return $content; } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 3ba143bef4..aa5e6911f1 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1788,16 +1788,6 @@ class ProjectsConsoleClientTest extends Scope $data = $this->setupProjectData(); $id = $data['projectId']; - /** Ensure SMTP is disabled */ - $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-response-format' => '1.9.4', - ], $this->getHeaders()), [ - 'enabled' => false, - ]); - $this->assertEquals(200, $response['headers']['status-code']); - /** Get Default Email Template */ $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([ 'content-type' => 'application/json', From 3721e6b95000dcded36649d19d53299e6fc5e0c1 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Mon, 11 May 2026 16:42:22 +0100 Subject: [PATCH 31/37] fix(migrations): write _APP_MIGRATION_HOST in generated .env (install & upgrade) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migrations worker (Appwrite→Appwrite migrations + CSV/JSON imports & exports) reads `_APP_MIGRATION_HOST` to call back into this instance's API. `_APP_MIGRATION_HOST` was introduced in #11229 (1.8.x / 1.9.x) but was never added to `app/config/variables.php`, so `appwrite install` / `appwrite upgrade` never write it into the generated `.env`. With the var unset, the migrations worker fails — on a fresh self-hosted install the first export hangs with no error (#11853). (Contributors and CI don't hit it because the repo's hand-maintained `.env` already has `_APP_MIGRATION_HOST=appwrite`; only the *installer-generated* `.env` is missing it.) Add `_APP_MIGRATION_HOST` to `app/config/variables.php` with default `appwrite` — the API service name in the standard Docker Compose setup, which is what the repo's own `.env` and the cloud Helm charts already use, and what the migration endpoint was hardcoded to before #11229. `appwrite install` and `appwrite upgrade` now write it into the generated `.env`, so fresh installs and upgrades have it set and the migration/import/export flows work. Scope: this PR fixes the install & upgrade paths only — it deliberately doesn't change the worker code. Fixes #11853 --- app/config/variables.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/config/variables.php b/app/config/variables.php index c834656ff4..90df9b4518 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -1336,6 +1336,15 @@ return [ 'category' => 'Migrations', 'description' => '', 'variables' => [ + [ + 'name' => '_APP_MIGRATION_HOST', + 'description' => 'Internal hostname the migrations worker uses to reach this instance\'s API (for migrations and CSV/JSON imports & exports). Defaults to \'appwrite\', the API service name in the standard Docker Compose setup. Only change this for non-standard deployments.', + 'introduction' => '1.9.0', + 'default' => 'appwrite', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID', 'description' => 'Google OAuth client ID. You can find it in your GCP application settings.', From 0e87d0b483c081c25b7e941fb7270123ca540186 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 12 May 2026 04:50:58 +0000 Subject: [PATCH 32/37] fix: remove invalid event label from project delete action Event::generateEvents() rejects the pattern 'project.delete' because it parses 'delete' as a resource that must be present in route params. The pre-migration route did not declare an event label, so functions and webhooks were never triggered on project deletion. Restore that behavior by removing the label. --- src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php index 0a60e4ce4d..4b26557ca9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php @@ -33,7 +33,6 @@ class Delete extends Action ->desc('Delete project') ->groups(['api', 'project']) ->label('scope', 'project.write') - ->label('event', 'project.delete') ->label('audits.event', 'project.delete') ->label('audits.resource', 'project/{project.$id}') ->label('sdk', new Method( From 5ef5ead98f255cf2480ca41546571f9c0abd485c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 12 May 2026 10:42:56 +0200 Subject: [PATCH 33/37] Fix function scheduler document missing --- .../Functions/Http/Functions/Update.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index b3fcb2c021..10e1f9c47f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -288,6 +288,28 @@ class Update extends Base // Inform scheduler if function is still active $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + + // Re-create schedule if missing + if ($schedule->isEmpty()) { + $schedule = $authorization->skip( + fn () => $dbForPlatform->createDocument('schedules', new Document([ + 'region' => $project->getAttribute('region'), + 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION, + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getSequence(), + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $function->getAttribute('schedule'), + 'active' => false, + ])) + ); + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'scheduleId' => $schedule->getId(), + 'scheduleInternalId' => $schedule->getSequence(), + ])); + } + $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) From ff92fd229b30a08849568d08b3c6c9e990e94fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 12 May 2026 10:43:11 +0200 Subject: [PATCH 34/37] Revert "Fix function scheduler document missing" This reverts commit 5ef5ead98f255cf2480ca41546571f9c0abd485c. --- .../Functions/Http/Functions/Update.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 10e1f9c47f..b3fcb2c021 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -288,28 +288,6 @@ class Update extends Base // Inform scheduler if function is still active $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - - // Re-create schedule if missing - if ($schedule->isEmpty()) { - $schedule = $authorization->skip( - fn () => $dbForPlatform->createDocument('schedules', new Document([ - 'region' => $project->getAttribute('region'), - 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION, - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getSequence(), - 'resourceUpdatedAt' => DateTime::now(), - 'projectId' => $project->getId(), - 'schedule' => $function->getAttribute('schedule'), - 'active' => false, - ])) - ); - - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ - 'scheduleId' => $schedule->getId(), - 'scheduleInternalId' => $schedule->getSequence(), - ])); - } - $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) From 8bef181ad8e8f947e0c717191c02ef5e881cadfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 12 May 2026 10:44:16 +0200 Subject: [PATCH 35/37] Fix scheduler doc missing for functions --- .../Functions/Http/Functions/Update.php | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index b3fcb2c021..e8713a179d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -287,7 +287,29 @@ class Update extends Base } // Inform scheduler if function is still active - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule = $authorization->skip(fn () => $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'))); + + // Re-create schedule if missing + if ($schedule->isEmpty()) { + $schedule = $authorization->skip( + fn () => $dbForPlatform->createDocument('schedules', new Document([ + 'region' => $project->getAttribute('region'), + 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION, + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getSequence(), + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $function->getAttribute('schedule'), + 'active' => false, + ])) + ); + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'scheduleId' => $schedule->getId(), + 'scheduleInternalId' => $schedule->getSequence(), + ])); + } + $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) From 548077f01ec541e21217846edf1a5f5344f04a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 12 May 2026 11:27:24 +0200 Subject: [PATCH 36/37] Fix flaky test --- tests/e2e/Services/Proxy/ProxyBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 32ee13b5f7..48de610365 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -209,8 +209,8 @@ trait ProxyBase $this->assertEquals(200, $rules['headers']['status-code']); $this->assertEquals(2, $rules['body']['total']); - $this->cleanupSite($siteId); $this->cleanupRule($ruleId); + $this->cleanupSite($siteId); } public function testCreateFunctionRule(): void From 930c23d7e3a55fd033fd6d4cfeff0d4aab7493d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 12 May 2026 12:27:06 +0200 Subject: [PATCH 37/37] Harden backwards compatibility attributes --- src/Appwrite/Utopia/Response/Filters/V26.php | 100 +++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php index 58ec380db8..3867ba907f 100644 --- a/src/Appwrite/Utopia/Response/Filters/V26.php +++ b/src/Appwrite/Utopia/Response/Filters/V26.php @@ -2,6 +2,7 @@ namespace Appwrite\Utopia\Response\Filters; +use Appwrite\Network\Platform; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filter; use Utopia\Config\Config; @@ -70,34 +71,95 @@ class V26 extends Filter $content['legalTaxId'] = $raw->getAttribute('legalTaxId', ''); $content['oAuthProviders'] = $this->expandOAuthProviders($raw); - $content['platforms'] = $raw->getAttribute('platforms', []); + + $content['platforms'] = []; + foreach ($raw->getAttribute('platforms', []) as $platform) { + $content['platforms'][] = $this->parsePlatform($platform); + } $content['webhooks'] = []; foreach ($raw->getAttribute('webhooks', []) as $webhook) { - - $webhook->setAttribute('tls', $webhook->getAttribute('security', true)); - $webhook->removeAttribute('security'); - - - $webhook->setAttribute('authUsername', $webhook->getAttribute('httpUser', '')); - $webhook->removeAttribute('httpUser'); - - - $webhook->setAttribute('authPassword', $webhook->getAttribute('httpPass', '')); - $webhook->removeAttribute('httpPass'); - - - $webhook->setAttribute('secret', $webhook->getAttribute('signatureKey', '')); - $webhook->removeAttribute('signatureKey'); - - $content['webhooks'][] = $webhook; + $content['webhooks'][] = $this->parseWebhook($webhook); } - $content['keys'] = $raw->getAttribute('keys', []); + $content['keys'] = []; + foreach ($raw->getAttribute('keys', []) as $key) { + $content['keys'][] = $this->parseKey($key); + } return $content; } + private function parsePlatform(Document $platform): array + { + $type = $platform->getAttribute('type', ''); + $key = $platform->getAttribute('key', ''); + + $result = [ + '$id' => $platform->getAttribute('$id', ''), + '$createdAt' => $platform->getAttribute('$createdAt', ''), + '$updatedAt' => $platform->getAttribute('$updatedAt', ''), + 'name' => $platform->getAttribute('name', ''), + 'type' => $type, + ]; + + switch ($type) { + case Platform::TYPE_ANDROID: + $result['applicationId'] = $key; + break; + case Platform::TYPE_APPLE: + $result['bundleIdentifier'] = $key; + break; + case Platform::TYPE_LINUX: + $result['packageName'] = $key; + break; + case Platform::TYPE_WINDOWS: + $result['packageIdentifierName'] = $key; + break; + default: + // Web and backwards-compatibility types are mapped to web + $result['hostname'] = $platform->getAttribute('hostname', ''); + $result['key'] = $key; + break; + } + + return $result; + } + + private function parseWebhook(Document $webhook): array + { + return [ + '$id' => $webhook->getAttribute('$id', ''), + '$createdAt' => $webhook->getAttribute('$createdAt', ''), + '$updatedAt' => $webhook->getAttribute('$updatedAt', ''), + 'name' => $webhook->getAttribute('name', ''), + 'url' => $webhook->getAttribute('url', ''), + 'events' => $webhook->getAttribute('events', []), + 'tls' => $webhook->getAttribute('security', true), + 'authUsername' => $webhook->getAttribute('httpUser', ''), + 'authPassword' => $webhook->getAttribute('httpPass', ''), + 'secret' => $webhook->getAttribute('signatureKey', ''), + 'enabled' => $webhook->getAttribute('enabled', true), + 'logs' => $webhook->getAttribute('logs', ''), + 'attempts' => $webhook->getAttribute('attempts', 0), + ]; + } + + private function parseKey(Document $key): array + { + return [ + '$id' => $key->getAttribute('$id', ''), + '$createdAt' => $key->getAttribute('$createdAt', ''), + '$updatedAt' => $key->getAttribute('$updatedAt', ''), + 'name' => $key->getAttribute('name', ''), + 'expire' => $key->getAttribute('expire', ''), + 'scopes' => $key->getAttribute('scopes', []), + 'secret' => $key->getAttribute('secret', ''), + 'accessedAt' => $key->getAttribute('accessedAt', ''), + 'sdks' => $key->getAttribute('sdks', []), + ]; + } + private function expandAuthMethods(array &$content): void { $authMethods = [];